[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".github/workflows/api.yml",
    "content": "name: Update API\non:\n  create:\n    tags:\n      - '*.*.*'\npermissions: {}\njobs:\n  api:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Start logux.org re-build\n        run: |\n          curl -XPOST -u \"${{ secrets.DEPLOY_USER }}:${{ secrets.DEPLOY_TOKEN }}\" -H \"Accept: application/vnd.github.everest-preview+json\" -H \"Content-Type: application/json\" https://api.github.com/repos/logux/logux.org/dispatches --data '{\"event_type\": \"deploy\"}'\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    tags:\n      - '*'\npermissions:\n  contents: write\njobs:\n  release:\n    name: Release On Tag\n    if: startsWith(github.ref, 'refs/tags/')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Extract the changelog\n        id: changelog\n        run: |\n          TAG_NAME=${GITHUB_REF/refs\\/tags\\//}\n          READ_SECTION=false\n          CHANGELOG=\"\"\n          while IFS= read -r line; do\n            if [[ \"$line\" =~ ^#+\\ +(.*) ]]; then\n              if [[ \"${BASH_REMATCH[1]}\" == \"$TAG_NAME\" ]]; then\n                READ_SECTION=true\n              elif [[ \"$READ_SECTION\" == true ]]; then\n                break\n              fi\n            elif [[ \"$READ_SECTION\" == true ]]; then\n              CHANGELOG+=\"$line\"$'\\n'\n            fi\n          done < \"CHANGELOG.md\"\n          CHANGELOG=$(echo \"$CHANGELOG\" | awk '/./ {$1=$1;print}')\n          echo \"changelog_content<<EOF\" >> $GITHUB_OUTPUT\n          echo \"$CHANGELOG\" >> $GITHUB_OUTPUT\n          echo \"EOF\" >> $GITHUB_OUTPUT\n      - name: Create the release\n        if: steps.changelog.outputs.changelog_content != ''\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0\n        with:\n          name: ${{ github.ref_name }}\n          body: '${{ steps.changelog.outputs.changelog_content }}'\n          draft: false\n          prerelease: false\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\non:\n  push:\n    branches:\n      - main\n      - next\n  pull_request:\npermissions:\n  contents: read\njobs:\n  full:\n    name: Node.js Latest Full\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Install pnpm\n        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0\n        with:\n          version: 10\n      - name: Install Node.js\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: 25\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install --ignore-scripts\n      - name: Run tests\n        run: pnpm test\n  short:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version:\n          - 24\n          - 22\n    name: Node.js ${{ matrix.node-version }} Quick\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Install pnpm\n        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0\n        with:\n          version: 10\n      - name: Install Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install --ignore-scripts\n      - name: Run unit tests\n        run: pnpm vitest run\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n\ncoverage/\n"
  },
  {
    "path": ".npmignore",
    "content": "test/\ncoverage/\ntsconfig.json\n**/*.test.ts\n**/types.ts\n**/errors.ts\n\n__snapshots__\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "import loguxOxfmtConfig from '@logux/oxc-configs/fmt'\n\nexport default loguxOxfmtConfig\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\nThis project adheres to [Semantic Versioning](http://semver.org/).\n\n## 0.14 “Sliver of Straw”\n\n- Removed Node.js 18 support.\n- Removed backend control.\n- Moved to number as subprotocol and remove `Context#isSubprotocol()`.\n- Moved to Logux Core 0.10 and Logux Protocol 5.\n- Changed custom HTTP listener API.\n- Added API to use custom Logux server node class.\n- Added `Server#sendOnConnect()`.\n- Reduced dependencies.\n- Changed color auto-detection algorithm to `util.styleText`.\n- Fixed brute-force lock issue during tests.\n\n## 0.13.1\n\n- Fixed vulnerability audit by moving to `cookie` 0.7.\n\n## 0.13 “Seven Red Suns”\n\n- Removed Node.js 14 and Node.js 16 support.\n- Moved to Logux Core 0.9.\n- Added action processing queues (by @VladBrok).\n- Added `unauthenticated` event (by @erictheswift).\n\n## 0.12.10\n\n- Fixed another Node.js 14 regression.\n\n## 0.12.9\n\n- Fixed Node.js 14 regression.\n\n## 0.12.8\n\n- Replaced `ip` to fix vulnerability.\n- Updated dependencies.\n\n## 0.12.7\n\n- Moved `ip` to `2.x` to fix vulnerability.\n\n## 0.12.6\n\n- Fixed `x/changed` filter in `addSyncMap` (by Eduard Aksamitov).\n\n## 0.12.5\n\n- Fixed async action’s filter in channel (by Eduard Aksamitov).\n\n## 0.12.4\n\n- Fixed docs.\n\n## 0.12.3\n\n- Fixed multiple subscriptions with filters per node (by Eduard Aksamitov).\n- Fixed types (by Nikita Galaiko).\n\n## 0.12.2\n\n- Fixed `since` in `load` of `addSyncMap` (by Nikita Galaiko).\n\n## 0.12.1\n\n- Fixed `since` in `initial` of `addSyncMapFilter` (by Nikita Galaiko).\n\n## 0.12 “Looks to the Moon”\n\n- Dropped Node.js 12 support.\n- Moved to Logux Core 0.8.\n- Moved to `pino` 8.\n- Added `disableHttpServer` option.\n- Added `return false` support to `load` callback in `addSyncMap`.\n- Fixed data loading on subscription on `SyncMap` creation.\n\n## 0.11 “Five Pebbles”\n\n- Added `addSyncMap()` and `addSyncMapFilter()`.\n- Added colorization to action ID and client ID (by Bijela Gora).\n- Added `TestServer#expectError()`.\n- Added `since` to `TestClient#subscribe()`.\n- Reduced noise in server log.\n- Moved to `pino` 7 (by Bijela Gora).\n\n## 0.10.8\n\n- Fixed test server destroying on fatal error.\n\n## 0.10.7\n\n- Reduced dependencies.\n\n## 0.10.6\n\n- Fixed `Promise` support in channel’s `filter` (by Eduard Aksamitov).\n- Replaced `nanocolors` with `picocolors`.\n\n## 0.10.5\n\n- Fixed `Server#http()`.\n- Fixed types (by Eduard Aksamitov).\n\n## 0.10.4\n\n- Updated `nanocolors`.\n\n## 0.10.3\n\n- Replaced `colorette` with `nanocolors`.\n\n## 0.10.2\n\n- Fixed `accessAndProcess` on server’s action (by Aleksandr Slepchenkov).\n- Added warning about circular reference in action.\n- Marked `action` and `meta` in callbacks as read-only.\n\n## 0.10.1\n\n- Fixed channel name parameters parsing (by Aleksandr Slepchenkov).\n- Used `LoguxNotFoundError` from `@logux/actions`.\n\n## 0.10 “Doraemon”\n\n- Moved project to ESM-only type. Applications must use ESM too.\n- Dropped Node.js 10 support.\n- Moved health check to `/health`.\n- Added `Server#http()` for custom HTTP processing.\n- Added `unsubscribe` callback to `Server#channel` (by @erictheswift).\n- Added reverted action to `logux/undo` (by Eduard Aksamitov).\n- Added RegExp support to `BaseServer#type()` (by Taras Vozniuk).\n- Added `accessAndLoad` and `accessAndProcess` callbacks for REST integration.\n- Added `LoguxNotFoundError` error for `accessAndLoad` and `accessAndProcess`.\n- Added request functions and `wasNot403()` for REST integration.\n- Added `ServerClient#httpHeaders`.\n- Added support for returning string from `resend` callback.\n- Added `Server#subscribe()` to send `logux/subscribed` action.\n- Added `Server#autoloadModules()`.\n- Added `fileUrl` option for ESM servers.\n- Added `Server#logger` for custom log messages.\n- Added `meta.excludeClients`.\n- Added `TestServer#expectUndo()`.\n- Added `TestServer#expectDenied()`.\n- Added `TestClient#received()`.\n- Added `TestServer#expectWrongCredentials()`.\n- Added `TestClient#clientId` and `TestClient#userId`.\n- Added `filter` option to `TestClient#subscribe()`.\n- Added Logux logotype to `GET /`.\n- Removed `reporter` option (by Aleksandr Slepchenkov).\n- Removed `yargs` dependency (by Aleksandr Slepchenkov).\n- Fixed `:` symbol support for channel names.\n- Fixed types performance by replacing `type` to `interface`.\n\n## 0.9.6\n\n- Update `yargs`.\n\n## 0.9.5\n\n- Fixed sending server’s actions to backend.\n\n## 0.9.4\n\n- Fix using old action’s IDs in `Server#channel→load`.\n\n## 0.9.3\n\n- Do not process actions from `Server#channel→load` in `Server#type`.\n- Replace color output library.\n\n## 0.9.2\n\n- Fix cookie support (by Eduard Aksamitov).\n\n## 0.9.1\n\n- Reduce dependencies.\n\n## 0.9 “Robby the Robot”\n\n- Use WebSocket Protocol version 4.\n- Use Back-end Protocol version 4.\n- Replace `bunyan` logger with `pino` (by Alexander Slepchenkov).\n- Clean up logger options (by Alexander Slepchenkov).\n- Allow to return actions from `load` callback.\n- Add cookie-based authentication.\n- Add `Server#process()`.\n- Allow to use action creator in `Server#type()`.\n- Add `LOGUX_SUBPROTOCOL` and `LOGUX_SUPPORTS` environment variables support.\n- Add `Server#autoloadModules()` (by Andrey Berezhnoy).\n- Add `Context#headers`.\n- Add argument to `TestServer#connect()`.\n- Add `auth: false` option to `TestServer`.\n- Fix action double sending.\n- Fix infinite reconnecting on authentication error.\n- Fix multiple servers usage in tests.\n- Fix types.\n\n## 0.8.6\n\n- Add `BaseServer#options` types.\n\n## 0.8.5\n\n- `Context#sendBack` returns Promise until action will be re-send and processed.\n- Fix `Context#sendBack` typings.\n\n## 0.8.4\n\n- Fix back-end protocol check in HTTP request receiving.\n\n## 0.8.3\n\n- Make node IDs in `TestClient` shorter.\n\n## 0.8.2\n\n- Fix types.\n\n## 0.8.1\n\n- Call `resend` after `access` step in action processing.\n- Add special reason for unknown action or channel errors.\n- Fix `TestClient` error on unknown action or channel.\n- Allow to show log by passing `reporter: \"human\"` option to `TestServer`.\n- Fix calling `resend` on server’s own actions.\n- Fix types (by Andrey Berezhnoy).\n\n## 0.8 “Morpheus”\n\n- Rename `init` callback to `load` in `Server#channel()`.\n- Add `TestServer` and `TestClient` to test servers.\n- Add `filterMeta` helper.\n- Fix types.\n\n## 0.7.2\n\n- More flexible types for logger.\n\n## 0.7.1\n\n- Print to the log about denied control requests attempts.\n- Fix server options types.\n- Return status code 500 on control requests if server has no secret.\n\n## 0.7 “Eliza Cassan”\n\n- Use Logux Core 0.5 and WebSocket Protocol 3.\n- Use Back-end Protocol 3.\n- Use the same port for WebSocket and control.\n- Rename `LOGUX_CONTROL_PASSWORD` to `LOGUX_CONTROL_SECRET`.\n- Rename `opts.controlPassword` to `opts.controlSecret`.\n- User ID must be always a string.\n- Add IP address check for control requests.\n- Fix types.\n\n## 0.6.1\n\n- Keep context between steps.\n- Fix re-sending actions back to the author.\n\n## 0.6 “Helios”\n\n- Add ES modules support.\n- Add TypeScript definitions (by Kirill Neruchev).\n- Move API docs from JSDoc to TypeDoc.\n\n## 0.5.3\n\n- Fix Nano Events API.\n\n## 0.5.2\n\n- Fix subscriptions for clients follower.\n\n## 0.5.1\n\n- Fix JSDoc.\n\n## 0.5 “Icarus”\n\n- Add `Context#sendBack()` shortcut.\n- Add `finally` callback to `Server#type()`. and `Server#channel()`.\n- Add `resend` callback to `Server#type()`.\n- Use Backend Protocol 2.\n- Deny any re-send meta keys from clients (like `channels`).\n- Add singular re-send meta keys support (`channel`, `client`, etc).\n- Allow to listen `preadd` and `add` log events in `Server#on()`.\n- Use `error` as default reason in `Server#undo()`.\n- Set boolean `false` user ID on client IDs like `false:client:uuid`.\n\n## 0.4 “Daedalus”\n\n- Add `.env` support.\n\n## 0.3.4\n\n- Update dependencies.\n\n## 0.3.3\n\n- Improve popular error messages during server launch (by Igor Strebezhev).\n\n## 0.3.2\n\n- Fix backend proxy version (by Dmitry Salahutdinov).\n- Clean up code (by Vladimir Schedrin).\n\n## 0.3.1\n\n- Fix support for `unknownAction` and `unknownChannel` commands from backend.\n\n## 0.3 “SHODAN”\n\n- Rename project from `logux-server` to `@logux/server`.\n- Rename `meta.nodeIds` to `meta.nodes`.\n- Rename `Server#clients` to `Server#connected`.\n- Rename `Server#users` to `Server#userIds`.\n- Split subscription to `access`, `init`, and `filter` steps.\n- Add `ctx` to callbacks.\n- Remove Node.js 6 and 8 support.\n- `Server.loadOptions` now overrides default options.\n- Change default port from `:1337` to `:31337`.\n- Use Logux Core 0.3.\n- Add brute force protection.\n- Add built-in proxy mode.\n- Add HTTP health check API.\n- Answer `logux/processed` after action processing.\n- Add `ServerClient#clientId` and `meta.clients`.\n- Add warning about missed action callbacks.\n\n## 0.2.9\n\n- Use `ws` instead of `uWS`.\n\n## 0.2.8\n\n- Add protection against authentication brute force.\n\n## 0.2.7\n\n- Use `uWS` 9.x with Node.js 10 support.\n\n## 0.2.6\n\n- Use `yargs` 11.x.\n\n## 0.2.5\n\n- Allow to have `:` in user ID.\n\n## 0.2.4\n\n- Use `uWS` 9.x.\n\n## 0.2.3\n\n- Fix `key` option with `{ pem: … }` value on Node.js 9.\n\n## 0.2.2\n\n- Don’t destroy server again on error during destroy.\n\n## 0.2.1\n\n- Don’t show `unknownType` error on server actions without processor.\n- Better action and meta view in `human` log.\n\n## 0.2 “Neuromancer”\n\n- Use Logux Protocol 2.\n- Use Logux Core 0.2 and Logux Sync 0.2.\n- Rename `Client#id` to `Client#userId`.\n- Remove `BaseServer#once` method.\n- Check action’s node ID to have user ID.\n- Use `uws` instead of `ws` (by Anton Savoskin).\n- Use Nano ID for node ID.\n- Remove deprecated `upgradeReq` from `Client#remoteAddess`.\n- Use Chalk 2.0.\n- Add `BaseServer#type` method.\n- Add `BaseServer#channel` method.\n- Add `BaseServer#undo` method.\n- Add `BaseServer#sendAction` method.\n- Take options from CLI and environment variables (by Pavel Kovalyov).\n- Add production non-secure protocol warning (by Hanna Stoliar).\n- Add Bunyan log format support (by Anton Artamonov and Mateusz Derks).\n- Add `error` event.\n- Set `meta.server`, `meta.status` and `meta.subprotocol`.\n- Add `debug` message support (by Roman Fursov).\n- Add `BaseServer#nodeId` shortcut.\n- Add node ID conflict fixing.\n- Export `ALLOWED_META`.\n- Better start error description (by Grigory Moroz).\n- Show Client ID in log for non-authenticated users.\n- Fix docs (by Grigoriy Beziuk, Nick Mitin and Konstantin Krivlenia).\n- Always use English for `--help` message.\n- Add security note for server output in development mode.\n\n## 0.1.1\n\n- Fix custom HTTP server support.\n\n## 0.1 “Wintermute”\n\n- Initial release.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2016 Andrey Sitnik <andrey@sitnik.es>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Logux Server [![Cult Of Martians][cult-img]][cult]\n\n<img align=\"right\" width=\"95\" height=\"148\" title=\"Logux logotype\"\n     src=\"https://logux.org/branding/logotype.svg\">\n\nLogux is a new way to connect client and server. Instead of sending\nHTTP requests (e.g., AJAX and GraphQL) it synchronizes log of operations\nbetween client, server, and other clients.\n\n- **[Guide, recipes, and API](https://logux.org/)**\n- **[Issues](https://github.com/logux/logux/issues)**\n  and **[roadmap](https://github.com/orgs/logux/projects/1)**\n- **[Projects](https://logux.org/guide/architecture/parts/)**\n  inside Logux ecosystem\n\nThis repository contains Logux server with:\n\n- Framework to write own server.\n- Proxy between WebSocket and HTTP server on any other language.\n\n<a href=\"https://evilmartians.com/?utm_source=logux-server\">\n  <img src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\"\n       alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\">\n</a>\n\n[cult-img]: http://cultofmartians.com/assets/badges/badge.svg\n[cult]: http://cultofmartians.com/done.html\n\n### Logux Server as Framework\n\n```js\nimport { isFirstOlder } from '@logux/core'\nimport { dirname } from 'path'\nimport { Server } from '@logux/server'\n\nconst server = new Server(\n  Server.loadOptions(process, {\n    subprotocol: 1,\n    minSubprotocol: 1,\n    root: import.meta.dirname\n  })\n)\n\nserver.auth(async ({ userId, token }) => {\n  const user = await findUserByToken(token)\n  return !!user && userId === user.id\n})\n\nserver.channel('user/:id', {\n  access(ctx, action, meta) {\n    return ctx.params.id === ctx.userId\n  },\n  async load(ctx, action, meta) {\n    const user = await db.loadUser(ctx.params.id)\n    return { type: 'USER_NAME', name: user.name }\n  }\n})\n\nserver.type('CHANGE_NAME', {\n  access(ctx, action, meta) {\n    return action.user === ctx.userId\n  },\n  resend(ctx, action, meta) {\n    return { channel: `user/${ctx.userId}` }\n  },\n  async process(ctx, action, meta) {\n    if (isFirstOlder(lastNameChange(action.user), meta)) {\n      await db.changeUserName({ id: action.user, name: action.name })\n    }\n  }\n})\n\nserver.listen()\n```\n\n[documentation]: https://logux.org/\n"
  },
  {
    "path": "add-http-pages/hello.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Logux Server</title>\n    <style>\n      html {\n        height: 100%;\n      }\n      body {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        min-height: 100%;\n        margin: 0;\n      }\n      svg {\n        height: 25vh;\n      }\n    </style>\n  </head>\n  <body>\n    <a href=\"https://logux.org/\">\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 75 117\">\n        <g fill=\"none\" fill-rule=\"evenodd\">\n          <path d=\"M-1 0h78v117H-1z\" />\n          <g fill-rule=\"nonzero\">\n            <path\n              fill=\"#1F1F1F\"\n              d=\"M2.239 40.258a12.83 12.83 0 00-.237.351C-.01 43.745-.515 46.634.71 50.4c1.484 4.565 3.865 6.346 9.045 7.98 5.705 1.8 7.064 2.346 10.02 4.481a23.387 23.387 0 013.3 2.885l4.198-1.394a28.632 28.632 0 00-1.907-2.072 27.455 27.455 0 00-3.196-2.697c-3.434-2.48-5.054-3.132-11.182-5.065-4.052-1.279-5.4-2.287-6.4-5.364-.69-2.121-.605-3.62.245-5.313L2.24 40.259zm4.91-7.089a22.975 22.975 0 001.72-3.977c1.038-3.18 1.217-5.458 1.078-9.74-.107-3.276-.06-4.527.422-6.218.648-2.273 2.042-4.164 4.406-5.87 2.658-1.918 5.15-2.604 7.865-2.292a14.85 14.85 0 012.38.491L22.371 9.12a22.74 22.74 0 01-.2-.022c-1.679-.193-3.155.214-5.002 1.546-2.923 2.11-3.317 3.494-3.149 8.678.153 4.713-.057 7.394-1.276 11.123a27.342 27.342 0 01-2.995 6.317l-2.6-3.592zm21.16 39.381c2.508 3.086 5.035 4.444 9.219 4.45 4.565.007 7.157-1.607 9.898-5.325.343-.465 1.837-2.598 2.27-3.184.928-1.257 1.795-2.288 2.783-3.27a23.18 23.18 0 012.784-2.36c1.364-.986 2.405-1.636 3.689-2.22l.023-4.394c-2.399.892-3.9 1.74-6.107 3.335a27.246 27.246 0 00-3.27 2.772c-1.15 1.143-2.145 2.328-3.187 3.74-.472.639-1.968 2.773-2.271 3.185-2.037 2.762-3.501 3.674-6.606 3.669-2.252-.003-3.644-.484-5.016-1.796L28.31 72.55zm38.808-14.716c3.919-1.472 5.9-3.358 7.234-7.431 1.29-3.939.765-6.885-1.353-10.092-.589-.892-2.979-4.007-3.421-4.634-1.48-2.097-2.532-4.081-3.317-6.485-.492-1.506-.799-2.81-.98-4.225l-4.217-1.334c.15 2.408.528 4.384 1.32 6.811.922 2.82 2.16 5.154 3.858 7.56.52.738 2.858 3.785 3.35 4.53 1.47 2.226 1.77 3.91.885 6.614-.716 2.185-1.57 3.35-3.336 4.265l-.023 4.421zm-2.09-41.45c-.22-4.341-1.311-6.541-4.74-9.019-2.339-1.69-4.597-2.387-7.035-2.254-1.101.06-2.206.28-3.518.666-.497.145-1.005.309-1.688.537.011-.003-1.32.446-1.709.575-3.303 1.09-5.743 1.588-8.806 1.588-1.515 0-2.87-.126-4.248-.39l-2.646 3.552c2.286.6 4.406.89 6.894.89 3.57 0 6.408-.579 10.09-1.794 4.765-1.573 4.538-1.506 5.854-1.578 1.481-.08 2.82.332 4.416 1.485 1.811 1.31 2.611 2.37 2.927 4.41l4.21 1.331z\"\n            />\n            <path\n              fill=\"#F5A623\"\n              d=\"M72.859 36.742c.095-.137.175-.256.236-.351 2.014-3.136 2.517-6.025 1.294-9.79-1.484-4.565-3.865-6.346-9.046-7.98-5.705-1.8-7.063-2.346-10.019-4.481a23.387 23.387 0 01-3.3-2.885l-4.198 1.394a28.632 28.632 0 001.907 2.072c.975.96 2.03 1.855 3.196 2.697 3.434 2.48 5.054 3.132 11.182 5.065 4.052 1.279 5.4 2.287 6.4 5.364.69 2.121.605 3.62-.245 5.313l2.593 3.582zm-4.91 7.089a22.975 22.975 0 00-1.72 3.977c-1.039 3.18-1.217 5.458-1.078 9.74.106 3.276.06 4.527-.423 6.218-.647 2.273-2.042 4.164-4.406 5.87-2.657 1.918-5.15 2.604-7.864 2.292a14.85 14.85 0 01-2.38-.491l2.649-3.556.2.022c1.678.193 3.155-.214 5.002-1.546 2.923-2.11 3.317-3.494 3.149-8.678-.154-4.713.057-7.394 1.276-11.123a27.342 27.342 0 012.994-6.317l2.6 3.592zM46.788 4.45C44.28 1.364 41.754.006 37.57 0c-4.566-.007-7.157 1.607-9.899 5.325-.343.465-1.837 2.598-2.27 3.184-.928 1.257-1.794 2.288-2.783 3.27a23.18 23.18 0 01-2.784 2.36c-1.363.986-2.404 1.636-3.688 2.22l-.023 4.394c2.398-.892 3.899-1.74 6.107-3.335a27.246 27.246 0 003.27-2.772c1.15-1.143 2.145-2.328 3.187-3.74.472-.639 1.967-2.773 2.271-3.185 2.036-2.762 3.5-3.674 6.606-3.669 2.252.003 3.644.484 5.015 1.796l4.209-1.398zM7.98 19.166c-3.918 1.472-5.899 3.358-7.233 7.431-1.29 3.939-.765 6.885 1.352 10.092.59.892 2.98 4.007 3.422 4.634C7 43.42 8.052 45.404 8.838 47.808c.492 1.506.798 2.81.979 4.225l4.218 1.334c-.15-2.408-.528-4.384-1.321-6.811-.922-2.82-2.16-5.154-3.857-7.56-.521-.738-2.859-3.785-3.351-4.53-1.47-2.226-1.77-3.91-.884-6.614.715-2.185 1.57-3.35 3.335-4.265l.023-4.421zm2.09 41.45c.22 4.341 1.312 6.541 4.74 9.019 2.34 1.69 4.598 2.387 7.036 2.254 1.1-.06 2.205-.28 3.518-.666a45.704 45.704 0 001.687-.537c-.01.003 1.32-.446 1.71-.575 3.303-1.09 5.743-1.588 8.806-1.588 1.515 0 2.87.126 4.248.39l2.646-3.552c-2.286-.6-4.406-.89-6.894-.89-3.57 0-6.409.579-10.09 1.794-4.766 1.573-4.538 1.506-5.854 1.578-1.482.08-2.82-.332-4.417-1.485-1.81-1.31-2.61-2.37-2.926-4.41l-4.21-1.331z\"\n            />\n          </g>\n          <path\n            fill=\"#1F1F1F\"\n            fill-rule=\"nonzero\"\n            d=\"M10.889 117v-4H6.944V89H3v28h7.889zm11.833-14c0 .547-.194 1.017-.582 1.41-.387.393-.85.59-1.39.59-.54 0-1.003-.197-1.39-.59a1.938 1.938 0 01-.582-1.41c0-.547.194-1.017.582-1.41.387-.393.85-.59 1.39-.59.54 0 1.003.197 1.39.59.388.393.582.863.582 1.41zm-7.889 0c0 1.653.579 3.067 1.736 4.24s2.55 1.76 4.181 1.76c1.63 0 3.024-.587 4.181-1.76s1.736-2.587 1.736-4.24-.579-3.067-1.736-4.24S22.381 97 20.75 97c-1.63 0-3.024.587-4.181 1.76s-1.736 2.587-1.736 4.24zm11.834-14H14.833v4h11.834v-4zm0 24H14.833v4h11.834v-4zM38.5 95c0 .547-.194 1.017-.582 1.41-.388.393-.851.59-1.39.59-.54 0-1.003-.197-1.39-.59a1.938 1.938 0 01-.582-1.41c0-.547.193-1.017.581-1.41.388-.393.852-.59 1.39-.59.54 0 1.003.197 1.391.59.388.393.582.863.582 1.41zm-3.944 16v-2.32c.618.213 1.275.32 1.972.32 1.63 0 3.024-.587 4.18-1.76 1.158-1.173 1.736-2.587 1.736-4.24 0-1.547-.506-2.88-1.518-4 1.012-1.12 1.518-2.453 1.518-4 0-1.653-.578-3.067-1.735-4.24S38.159 89 36.528 89c-1.63 0-3.024.587-4.181 1.76S30.61 93.347 30.61 95s.579 3.067 1.736 4.24 2.55 1.76 4.18 1.76c.54 0 1.003.197 1.391.59.388.393.582.863.582 1.41 0 .547-.194 1.017-.582 1.41-.388.393-.851.59-1.39.59-.54 0-1.003-.197-1.39-.59a1.938 1.938 0 01-.582-1.41H30.61v8c0 1.653.579 3.067 1.736 4.24s2.55 1.76 4.18 1.76c1.631 0 3.025-.587 4.182-1.76 1.157-1.173 1.735-2.587 1.735-4.24H38.5c0 .547-.194 1.017-.582 1.41-.388.393-.851.59-1.39.59-.54 0-1.003-.197-1.39-.59a1.938 1.938 0 01-.582-1.41zm11.833 0c0 1.653.578 3.067 1.735 4.24s2.551 1.76 4.182 1.76c1.63 0 3.024-.587 4.18-1.76 1.158-1.173 1.736-2.587 1.736-4.24V89h-3.944v22c0 .547-.194 1.017-.582 1.41-.388.393-.851.59-1.39.59-.54 0-1.003-.197-1.39-.59a1.938 1.938 0 01-.583-1.41V89H46.39v22zM74 89H62.167v4H74v-4zm-3.116 14l2.78-2.82-2.8-2.82-2.78 2.82-2.782-2.82-2.8 2.82 2.8 2.82-2.8 2.84 2.8 2.82 2.781-2.82 2.781 2.82 2.8-2.82-2.78-2.84zM74 113H62.167v4H74v-4z\"\n          />\n        </g>\n      </svg>\n    </a>\n  </body>\n</html>\n"
  },
  {
    "path": "add-http-pages/index.js",
    "content": "import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nlet hello\nasync function readHello() {\n  if (!hello) {\n    hello = await readFile(join(import.meta.dirname, 'hello.html'))\n  }\n  return hello\n}\n\nexport function addHttpPages(server) {\n  if (!server.options.disableHttpServer) {\n    server.http('GET', '/', async (req, res) => {\n      let data = await readHello()\n      res.writeHead(200, { 'Content-Type': 'text/html' })\n      res.end(data)\n    })\n    server.http('GET', '/health', (req, res) => {\n      res.writeHead(200, { 'Content-Type': 'text/plain' })\n      res.end('Logux Server: OK\\n')\n    })\n  }\n}\n"
  },
  {
    "path": "add-http-pages/index.test.ts",
    "content": "import { type TestLog, TestTime } from '@logux/core'\nimport http from 'node:http'\nimport { setTimeout } from 'node:timers/promises'\nimport { afterEach, expect, it } from 'vitest'\n\nimport {\n  BaseServer,\n  type BaseServerOptions,\n  type ServerMeta\n} from '../index.js'\n\nconst DEFAULT_OPTIONS = {\n  minSubprotocol: 0,\n  subprotocol: 0\n}\n\nlet lastPort = 9111\n\nfunction createServer(\n  options: Partial<BaseServerOptions> = {}\n): BaseServer<object, TestLog<ServerMeta>> {\n  let opts = {\n    ...DEFAULT_OPTIONS,\n    ...options\n  }\n  if (typeof opts.time === 'undefined') {\n    opts.time = new TestTime()\n    opts.id = 'uuid'\n  }\n  if (typeof opts.port === 'undefined') {\n    lastPort += 1\n    opts.port = lastPort\n  }\n\n  let created = new BaseServer<object, TestLog<ServerMeta>>(opts)\n  created.auth(() => true)\n\n  destroyable = created\n\n  return created\n}\n\nlet destroyable: BaseServer | undefined\n\nclass RequestError extends Error {\n  statusCode: number | undefined\n\n  constructor(statusCode: number | undefined, body: string) {\n    super(body)\n    this.name = 'RequestError'\n    this.statusCode = statusCode\n  }\n}\n\ninterface HttpResponse {\n  body: string\n  headers: http.IncomingHttpHeaders\n}\n\nfunction request(\n  server: BaseServer,\n  method: string,\n  path: string\n): Promise<HttpResponse> {\n  return new Promise<HttpResponse>((resolve, reject) => {\n    let req = http.request(\n      {\n        host: '127.0.0.1',\n        method,\n        path,\n        port: server.options.port\n      },\n      res => {\n        let body = ''\n        res.on('data', chunk => {\n          body += chunk\n        })\n        res.on('end', () => {\n          if (res.statusCode === 200) {\n            resolve({ body, headers: res.headers })\n          } else {\n            let error = new RequestError(res.statusCode, body)\n            reject(error)\n          }\n        })\n      }\n    )\n    req.on('error', reject)\n    req.end()\n  })\n}\n\nasync function requestError(\n  server: BaseServer,\n  method: string,\n  path: string\n): Promise<RequestError> {\n  try {\n    await request(server, method, path)\n  } catch (e) {\n    if (e instanceof RequestError) return e\n  }\n  throw new Error('Error was not found')\n}\n\nafterEach(async () => {\n  if (destroyable) {\n    await destroyable.destroy()\n    destroyable = undefined\n  }\n})\n\nit('has hello page', async () => {\n  let app = createServer({})\n  await app.listen()\n  let response = await request(app, 'GET', '/')\n  expect(response.body).toContain('Logux Server')\n  expect(response.body).toContain('<svg ')\n})\n\nit('disables HTTP on request', async () => {\n  let app = createServer({ disableHttpServer: true })\n  await app.listen()\n  let response = false\n  let req = http.request(\n    {\n      host: '127.0.0.1',\n      method: 'GET',\n      path: '/health',\n      port: app.options.port\n    },\n    () => {\n      response = true\n    }\n  )\n  req.on('error', () => {})\n  await setTimeout(100)\n  expect(response).toBe(false)\n  req.destroy()\n})\n\nit('has health check', async () => {\n  let app = createServer()\n  await app.listen()\n  let response = await request(app, 'GET', '/health')\n  expect(response.body).toContain('OK')\n})\n\nit('responses 404', async () => {\n  let app = createServer()\n  await app.listen()\n  let err = await requestError(app, 'GET', '/unknown')\n  expect(err.statusCode).toEqual(404)\n  expect(err.message).toEqual('Not found\\n')\n})\n\nit('has custom HTTP processor', async () => {\n  let app = createServer()\n  let unknownGet = 0\n  let unknownRest = 0\n  app.http('POST', '/a', (req, res) => {\n    res.end('POST a')\n  })\n  app.http('GET', '/a', (req, res) => {\n    res.end('GET a')\n  })\n  app.http('GET', '/b', (req, res) => {\n    res.end('GET b')\n  })\n  app.http((req, res) => {\n    if (req.method === 'GET') {\n      res.end('GET unknown')\n      unknownGet += 1\n      return true\n    } else {\n      return false\n    }\n  })\n  app.http((req, res) => {\n    if (req.url !== '/404') {\n      res.end('unknown')\n      unknownRest += 1\n      return true\n    } else {\n      return false\n    }\n  })\n  await app.listen()\n  expect((await request(app, 'GET', '/a')).body).toContain('GET a')\n  expect((await request(app, 'GET', '/a%3Fsecret')).body).toContain('GET a')\n  expect((await request(app, 'GET', '/b')).body).toContain('GET b')\n  expect((await request(app, 'POST', '/a')).body).toContain('POST a')\n  expect((await request(app, 'GET', '/c')).body).toContain('GET unknown')\n  expect((await request(app, 'GET', '/d')).body).toContain('GET unknown')\n  expect((await request(app, 'POST', '/e')).body).toContain('unknown')\n  expect((await requestError(app, 'POST', '/404')).statusCode).toEqual(404)\n  expect(unknownGet).toEqual(2)\n  expect(unknownRest).toEqual(1)\n})\n\nit('warns that HTTP is disables', () => {\n  let app = createServer({ disableHttpServer: true })\n  expect(() => {\n    app.http(() => true)\n  }).toThrow(/when `disableHttpServer` enabled/)\n})\n\nit('waits until all HTTP processing ends', async () => {\n  let app = createServer()\n  let resolveA: (() => void) | undefined\n  app.http('GET', '/a', () => {\n    return new Promise(resolve => {\n      resolveA = resolve\n    })\n  })\n  let resolveResult: ((processed: boolean) => void) | undefined\n  app.http(() => {\n    return new Promise(resolve => {\n      resolveResult = resolve\n    })\n  })\n  await app.listen()\n\n  request(app, 'GET', '/a')\n  request(app, 'GET', '/other')\n  await setTimeout(10)\n\n  let destroyed = false\n  app.destroy().then(() => {\n    destroyed = true\n  })\n\n  await setTimeout(100)\n  expect(destroyed).toBe(false)\n\n  expect((await requestError(app, 'POST', '/a')).message).toEqual(\n    'The server is shutting down\\n'\n  )\n\n  resolveA!()\n  await setTimeout(100)\n  expect(destroyed).toBe(false)\n\n  resolveResult!(true)\n  await setTimeout(100)\n  expect(destroyed).toBe(true)\n})\n"
  },
  {
    "path": "add-sync-map/index.d.ts",
    "content": "import type {\n  LoguxSubscribeAction,\n  SyncMapChangeAction,\n  SyncMapChangedAction,\n  SyncMapCreateAction,\n  SyncMapCreatedAction,\n  SyncMapDeleteAction,\n  SyncMapDeletedAction,\n  SyncMapTypes,\n  SyncMapValues\n} from '@logux/actions'\n\nimport type { BaseServer, ServerMeta } from '../base-server/index.js'\nimport type { Context } from '../context/index.js'\n\ndeclare const WITH_TIME: unique symbol\n\nexport type WithTime<Value extends SyncMapTypes | SyncMapTypes[]> = {\n  time: number\n  value: Value\n  [WITH_TIME]: true\n}\n\nexport type WithoutTime<Value extends SyncMapTypes | SyncMapTypes[]> = {\n  time: undefined\n  value: Value\n  [WITH_TIME]: false\n}\n\nexport type SyncMapData<Value extends SyncMapValues> = {\n  [Key in keyof Value]: WithoutTime<Value[Key]> | WithTime<Value[Key]>\n} & { id: string }\n\n/**\n * Add last changed time to value to use in conflict resolution.\n *\n * If you do not know the time, use {@link NoConflictResolution}.\n *\n * @param value The value.\n * @param time UNIX milliseconds.\n * @returns Wrapper.\n */\nexport function ChangedAt<Value extends SyncMapTypes | SyncMapTypes[]>(\n  value: Value,\n  time: number\n): WithTime<Value>\n\n/**\n * Mark that the value has no last changed date and conflict resolution\n * can’t be applied.\n *\n * @param value The value.\n * @returns Wrapper.\n */\nexport function NoConflictResolution<\n  Value extends SyncMapTypes | SyncMapTypes[]\n>(value: Value): WithTime<Value>\n\ninterface SyncMapActionFilter<Value extends SyncMapValues> {\n  (\n    ctx: Context,\n    action:\n      | SyncMapChangedAction<Value>\n      | SyncMapCreatedAction<Value>\n      | SyncMapDeletedAction,\n    meta: ServerMeta\n  ): boolean | Promise<boolean>\n}\n\ninterface SyncMapOperations<Value extends SyncMapValues> {\n  access(\n    ctx: Context,\n    id: string,\n    action:\n      | LoguxSubscribeAction\n      | SyncMapChangeAction\n      | SyncMapCreateAction\n      | SyncMapDeleteAction,\n    meta: ServerMeta\n  ): boolean | Promise<boolean>\n\n  change?(\n    ctx: Context,\n    id: string,\n    fields: Partial<Value>,\n    time: number,\n    action: SyncMapChangeAction<Value>,\n    meta: ServerMeta\n  ): boolean | Promise<boolean | void> | void\n\n  create?(\n    ctx: Context,\n    id: string,\n    fields: Value,\n    time: number,\n    action: SyncMapCreateAction<Value>,\n    meta: ServerMeta\n  ): boolean | Promise<boolean | void> | void\n\n  delete?(\n    ctx: Context,\n    id: string,\n    action: SyncMapDeleteAction,\n    meta: ServerMeta\n  ): boolean | Promise<boolean | void> | void\n\n  load?(\n    ctx: Context,\n    id: string,\n    since: number | undefined,\n    action: LoguxSubscribeAction,\n    meta: ServerMeta\n  ): false | Promise<false | SyncMapData<Value>> | SyncMapData<Value>\n}\n\ninterface SyncMapFilterOperations<Value extends SyncMapValues> {\n  access?(\n    ctx: Context,\n    filter: Partial<Value> | undefined,\n    action: LoguxSubscribeAction,\n    meta: ServerMeta\n  ): boolean | Promise<boolean>\n\n  actions?(\n    ctx: Context,\n    filter: Partial<Value> | undefined,\n    action: LoguxSubscribeAction,\n    meta: ServerMeta\n  ): Promise<SyncMapActionFilter<Value>> | SyncMapActionFilter<Value> | void\n\n  initial(\n    ctx: Context,\n    filter: Partial<Value> | undefined,\n    since: number | undefined,\n    action: LoguxSubscribeAction,\n    meta: ServerMeta\n  ): Promise<SyncMapData<Value>[]> | SyncMapData<Value>[]\n}\n\n/**\n * Add callbacks for client’s `SyncMap`.\n *\n * ```js\n * import { addSyncMap, isFirstTimeOlder, ChangedAt } from '@logux/server'\n * import { LoguxNotFoundError } from '@logux/actions'\n *\n * addSyncMap(server, 'tasks', {\n *   async access (ctx, id) {\n *     const task = await Task.find(id)\n *     return ctx.userId === task.authorId\n *   },\n *\n *   async load (ctx, id, since) {\n *     const task = await Task.find(id)\n *     if (!task) throw new LoguxNotFoundError()\n *     return {\n *       id: task.id,\n *       text: ChangedAt(task.text, task.textChanged),\n *       finished: ChangedAt(task.finished, task.finishedChanged),\n *     }\n *   },\n *\n *   async create (ctx, id, fields, time) {\n *     await Task.create({\n *       id,\n *       text: fields.text,\n *       finished: fields.finished,\n *       authorId: ctx.userId,\n *       textChanged: time,\n *       finishedChanged: time\n *     })\n *   },\n *\n *   async change (ctx, id, fields, time) {\n *     const task = await Task.find(id)\n *     if ('text' in fields) {\n *       if (task.textChanged < time) {\n *         await task.update({\n *           text: fields.text,\n *           textChanged: time\n *         })\n *       }\n *     }\n *     if ('finished' in fields) {\n *       if (task.finishedChanged < time) {\n *         await task.update({\n *           finished: fields.finished,\n *           finishedChanged: time\n *         })\n *       }\n *     }\n *   }\n *\n *   async delete (ctx, id) {\n *     await Task.delete(id)\n *   }\n * })\n * ```\n *\n * @param server Server instance.\n * @param plural Prefix for channel names and action types.\n * @param operations Callbacks.\n */\nexport function addSyncMap<Values extends SyncMapValues>(\n  server: BaseServer,\n  plural: string,\n  operations: SyncMapOperations<Values>\n): void\n\n/**\n * Add callbacks for client’s `useFilter`.\n *\n * ```js\n * import { addSyncMapFilter, ChangedAt } from '@logux/server'\n *\n * addSyncMapFilter(server, 'tasks', {\n *   access (ctx, filter) {\n *     return true\n *   },\n *\n *   initial (ctx, filter, since) {\n *     let tasks = await Tasks.where({ ...filter, authorId: ctx.userId })\n *     // You can return only data changed after `since`\n *     return tasks.map(task => ({\n *       id: task.id,\n *       text: ChangedAt(task.text, task.textChanged),\n *       finished: ChangedAt(task.finished, task.finishedChanged),\n *     }))\n *   },\n *\n *   actions (filterCtx, filter) {\n *     return (actionCtx, action, meta) => {\n *       return actionCtx.userId === filterCtx.userId\n *     }\n *   }\n * })\n * ```\n\n * @param server Server instance.\n * @param plural Prefix for channel names and action types.\n * @param operations Callbacks.\n */\nexport function addSyncMapFilter<Values extends SyncMapValues>(\n  server: BaseServer,\n  plural: string,\n  operations: SyncMapFilterOperations<Values>\n): void\n"
  },
  {
    "path": "add-sync-map/index.js",
    "content": "const WITH_TIME = Symbol('WITH_TIME')\n\nexport function ChangedAt(value, time) {\n  return { time, value, [WITH_TIME]: true }\n}\n\nexport function NoConflictResolution(value) {\n  return { value, [WITH_TIME]: false }\n}\n\nasync function addFinished(server, ctx, type, action, meta) {\n  await server.process(\n    { ...action, type },\n    { excludeClients: [ctx.clientId], time: meta.time }\n  )\n}\n\nfunction resendFinished(server, plural, type, all = true) {\n  if (all) {\n    server.type(type, {\n      access() {\n        return false\n      },\n      resend(ctx, action) {\n        return [plural, `${plural}/${action.id}`]\n      }\n    })\n  } else {\n    server.type(type, {\n      access() {\n        return false\n      },\n      resend(ctx, action) {\n        return [`${plural}/${action.id}`]\n      }\n    })\n  }\n}\n\nfunction buildFilter(filter) {\n  return (ctx, action) => {\n    if (action.type.endsWith('/created')) {\n      for (let key in filter) {\n        if (action.fields[key] !== filter[key]) return false\n      }\n    }\n    if (action.type.endsWith('/changed')) {\n      for (let key in filter) {\n        if (key in action.fields && action.fields[key] !== filter[key]) {\n          return false\n        }\n      }\n    }\n    return true\n  }\n}\n\nasync function sendMap(server, changedType, data, since) {\n  let { id, ...other } = data\n  let byTime = new Map()\n  for (let key in other) {\n    if (other[key][WITH_TIME] === true) {\n      let time = other[key].time\n      if (!byTime.has(time)) byTime.set(time, {})\n      byTime.get(time)[key] = other[key].value\n    } else if (other[key][WITH_TIME] === false) {\n      if (!byTime.has('now')) byTime.set('now', {})\n      byTime.get('now')[key] = other[key].value\n    } else {\n      throw new Error('Wrap value into ChangedAt() or NoConflictResolution()')\n    }\n  }\n  for (let [time, fields] of byTime.entries()) {\n    let changedMeta\n    if (time !== 'now') {\n      changedMeta = { time }\n      if (time < since) continue\n    }\n    await server.process(\n      {\n        fields,\n        id,\n        type: changedType\n      },\n      changedMeta\n    )\n  }\n}\n\nexport function addSyncMap(server, plural, operations) {\n  let createdType = `${plural}/created`\n  let changedType = `${plural}/changed`\n  let deletedType = `${plural}/deleted`\n  resendFinished(server, plural, createdType)\n  resendFinished(server, plural, changedType)\n  resendFinished(server, plural, deletedType, false)\n\n  if (operations.load) {\n    server.channel(`${plural}/:id`, {\n      access(ctx, action, meta) {\n        return operations.access(ctx, ctx.params.id, action, meta)\n      },\n      async load(ctx, action, meta) {\n        if (action.creating) return\n        let since = action.since ? action.since.time : 0\n        let data = await operations.load(\n          ctx,\n          ctx.params.id,\n          since,\n          action,\n          meta\n        )\n        if (data !== false) {\n          await sendMap(server, changedType, data, since)\n        }\n      }\n    })\n  }\n\n  if (operations.create) {\n    server.type(`${plural}/create`, {\n      access(ctx, action, meta) {\n        return operations.access(ctx, action.id, action, meta)\n      },\n      async process(ctx, action, meta) {\n        let result = await operations.create(\n          ctx,\n          action.id,\n          action.fields,\n          meta.time,\n          action,\n          meta\n        )\n        if (result !== false) {\n          await addFinished(server, ctx, createdType, action, meta)\n        }\n      }\n    })\n  }\n\n  if (operations.change) {\n    server.type(`${plural}/change`, {\n      access(ctx, action, meta) {\n        return operations.access(ctx, action.id, action, meta)\n      },\n      async process(ctx, action, meta) {\n        let result = await operations.change(\n          ctx,\n          action.id,\n          action.fields,\n          meta.time,\n          action,\n          meta\n        )\n        if (result !== false) {\n          await addFinished(server, ctx, changedType, action, meta)\n        }\n      }\n    })\n  }\n\n  if (operations.delete) {\n    server.type(`${plural}/delete`, {\n      access(ctx, action, meta) {\n        return operations.access(ctx, action.id, action, meta)\n      },\n      async process(ctx, action, meta) {\n        let result = await operations.delete(ctx, action.id, action, meta)\n        if (result !== false) {\n          await addFinished(server, ctx, deletedType, action, meta)\n        }\n      }\n    })\n  }\n}\n\nexport function addSyncMapFilter(server, plural, operations) {\n  let changedType = `${plural}/changed`\n\n  server.channel(plural, {\n    access(ctx, action, meta) {\n      return operations.access(ctx, action.filter, action, meta)\n    },\n    filter(ctx, action, meta) {\n      let filter = action.filter ? buildFilter(action.filter) : () => true\n      let custom = operations.actions\n        ? operations.actions(ctx, action.filter, action, meta)\n        : () => true\n      return (ctx2, action2, meta2) => {\n        return filter(ctx2, action2, meta2) && custom(ctx2, action2, meta2)\n      }\n    },\n    async load(ctx, action, meta) {\n      let since = action.since ? action.since.time : 0\n      let data = await operations.initial(\n        ctx,\n        action.filter,\n        since,\n        action,\n        meta\n      )\n      await Promise.all(\n        data.map(async i => {\n          await server.subscribe(ctx.nodeId, `${plural}/${i.id}`)\n          await sendMap(server, changedType, i, since)\n        })\n      )\n    }\n  })\n}\n"
  },
  {
    "path": "add-sync-map/index.test.ts",
    "content": "import {\n  defineSyncMapActions,\n  LoguxNotFoundError,\n  loguxProcessed,\n  loguxSubscribed\n} from '@logux/actions'\nimport { setTimeout } from 'node:timers/promises'\nimport { afterEach, expect, it } from 'vitest'\n\nimport {\n  addSyncMap,\n  addSyncMapFilter,\n  ChangedAt,\n  NoConflictResolution,\n  type SyncMapData,\n  type TestClient,\n  TestServer\n} from '../index.js'\n\ntype TaskValue = {\n  finished: boolean\n  text: string\n}\n\ntype TaskRecord = {\n  finishedChanged: number\n  textChanged: number\n} & TaskValue\n\nlet [\n  createTask,\n  changeTask,\n  deleteTask,\n  createdTask,\n  changedTask,\n  deletedTask\n] = defineSyncMapActions('tasks')\n\ntype CommentValue = {\n  author?: string\n  text?: string\n}\nlet [\n  createComment,\n  changeComment,\n  deleteComment,\n  createdComment,\n  changedComment,\n  deletedComment\n] = defineSyncMapActions('comments')\n\nlet tasks = new Map<string, TaskRecord>()\n\nlet destroyable: TestServer | undefined\n\nfunction getTime(client: TestClient, creator: { type: string }): number[] {\n  return client.log\n    .entries()\n    .filter(([action]) => action.type === creator.type)\n    .map(([, meta]) => meta.time)\n}\n\nfunction getServer(): TestServer {\n  let server = new TestServer()\n  destroyable = server\n  addSyncMap<TaskValue>(server, 'tasks', {\n    access(ctx, id, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      return ctx.userId !== 'wrong' && id !== 'bad'\n    },\n    change(ctx, id, fields, time, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      expect(typeof ctx.userId).toBe('string')\n      let task = tasks.get(id)!\n      if (\n        typeof fields.finished !== 'undefined' &&\n        task.finishedChanged < time\n      ) {\n        task.finished = fields.finished\n        task.finishedChanged = time\n      }\n      if (typeof fields.text !== 'undefined' && task.textChanged < time) {\n        task.text = fields.text\n        task.textChanged = time\n      }\n    },\n    create(ctx, id, fields, time, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      expect(typeof ctx.userId).toBe('string')\n      tasks.set(id, {\n        ...fields,\n        finishedChanged: time,\n        textChanged: time\n      })\n    },\n    delete(ctx, id, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      expect(typeof ctx.userId).toBe('string')\n      tasks.delete(id)\n    },\n    load(ctx, id, since, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      expect(typeof ctx.userId).toBe('string')\n      let task = tasks.get(id)\n      if (!task) throw new LoguxNotFoundError()\n      return {\n        finished: ChangedAt(task.finished, task.finishedChanged),\n        id,\n        text: ChangedAt(task.text, task.textChanged)\n      }\n    }\n  })\n  addSyncMapFilter<TaskValue>(server, 'tasks', {\n    access(ctx, filter, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      if (ctx.userId === 'wrong') return false\n      if (filter?.text) return false\n      return true\n    },\n    actions(ctx, filter, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      return (ctx2, action2) => action2.id !== 'silence'\n    },\n    initial(ctx, filter, since, action, meta) {\n      expect(typeof action.type).toBe('string')\n      expect(typeof meta.id).toBe('string')\n      let selected: SyncMapData<TaskValue>[] = []\n      for (let [id, task] of tasks.entries()) {\n        if (filter) {\n          let filterKeys = Object.keys(filter) as (keyof TaskValue)[]\n          if (filterKeys.some(i => task[i] !== filter[i])) {\n            continue\n          }\n        }\n        selected.push({\n          finished: ChangedAt(task.finished, task.finishedChanged),\n          id,\n          text: ChangedAt(task.text, task.textChanged)\n        })\n      }\n      return selected\n    }\n  })\n  return server\n}\n\nafterEach(() => {\n  destroyable?.destroy()\n  tasks.clear()\n})\n\nit('checks SyncMap access', async () => {\n  let server = getServer()\n\n  let wrong = await server.connect('wrong')\n  await server.expectDenied(() => wrong.subscribe('tasks/10'))\n  await server.expectDenied(() => wrong.subscribe('tasks'))\n\n  let correct = await server.connect('10')\n  await server.expectDenied(() => correct.subscribe('tasks/bad'))\n  await server.expectDenied(() => correct.subscribe('tasks', { text: 'A' }))\n  await server.expectDenied(() =>\n    correct.process(\n      createdTask({ fields: { finished: false, text: 'One' }, id: '10' })\n    )\n  )\n  await server.expectDenied(() => correct.process(deletedTask({ id: '10' })))\n})\n\nit('supports 404', async () => {\n  let server = getServer()\n  let client = await server.connect('1')\n  await server.expectUndo('notFound', () => client.subscribe('tasks/10'))\n})\n\nit('supports SyncMap', async () => {\n  let server = getServer()\n  let client1 = await server.connect('1')\n  let client2 = await server.connect('2')\n\n  client1.log.keepActions()\n  client2.log.keepActions()\n\n  await client1.process(\n    createTask({ fields: { finished: false, text: 'One' }, id: '10' })\n  )\n  expect(Object.fromEntries(tasks)).toEqual({\n    10: { finished: false, finishedChanged: 1, text: 'One', textChanged: 1 }\n  })\n\n  expect(await client1.subscribe('tasks/10')).toEqual([\n    changedTask({ fields: { finished: false, text: 'One' }, id: '10' })\n  ])\n  expect(getTime(client1, changedTask)).toEqual([1])\n  await client2.subscribe('tasks/10')\n\n  expect(\n    await client2.collect(() =>\n      client1.process(changeTask({ fields: { text: 'One1' }, id: '10' }))\n    )\n  ).toEqual([changedTask({ fields: { text: 'One1' }, id: '10' })])\n  expect(Object.fromEntries(tasks)).toEqual({\n    10: { finished: false, finishedChanged: 1, text: 'One1', textChanged: 10 }\n  })\n  expect(getTime(client2, changedTask)).toEqual([1, 10])\n\n  expect(\n    await client1.collect(async () => {\n      await client1.process(changeTask({ fields: { text: 'One2' }, id: '10' }))\n    })\n  ).toEqual([loguxProcessed({ id: '13 1:1:1 0' })])\n\n  await client1.process(changeTask({ fields: { text: 'One0' }, id: '10' }), {\n    time: 12\n  })\n  expect(Object.fromEntries(tasks)).toEqual({\n    10: { finished: false, finishedChanged: 1, text: 'One2', textChanged: 13 }\n  })\n\n  let client3 = await server.connect('3')\n  expect(\n    await client3.subscribe('tasks/10', undefined, { id: '', time: 12 })\n  ).toEqual([changedTask({ fields: { text: 'One2' }, id: '10' })])\n\n  let client4 = await server.connect('3')\n  expect(\n    await client4.subscribe('tasks/10', undefined, { id: '', time: 20 })\n  ).toEqual([])\n})\n\nit('supports SyncMap filters', async () => {\n  let server = getServer()\n\n  let client1 = await server.connect('1')\n  let client2 = await server.connect('2')\n\n  expect(await client1.subscribe('tasks')).toEqual([])\n  expect(\n    await client1.process(\n      createTask({ fields: { finished: false, text: 'One' }, id: '1' })\n    )\n  ).toEqual([loguxProcessed({ id: '3 1:1:1 0' })])\n  await client1.process(\n    createTask({ fields: { finished: true, text: 'Two' }, id: '2' })\n  )\n  await client1.process(\n    createTask({ fields: { finished: false, text: 'Three' }, id: '3' })\n  )\n\n  expect(await client2.subscribe('tasks', { finished: false })).toEqual([\n    loguxSubscribed({ channel: 'tasks/1' }),\n    loguxSubscribed({ channel: 'tasks/3' }),\n    changedTask({ fields: { finished: false, text: 'One' }, id: '1' }),\n    changedTask({ fields: { finished: false, text: 'Three' }, id: '3' })\n  ])\n\n  expect(\n    await client2.collect(async () => {\n      await client1.process(changeTask({ fields: { text: 'One1' }, id: '1' }))\n    })\n  ).toEqual([changedTask({ fields: { text: 'One1' }, id: '1' })])\n\n  expect(\n    await client2.collect(async () => {\n      await client1.process(deleteTask({ id: '3' }))\n    })\n  ).toEqual([deletedTask({ id: '3' })])\n  expect(Object.fromEntries(tasks)).toEqual({\n    1: { finished: false, finishedChanged: 3, text: 'One1', textChanged: 18 },\n    2: { finished: true, finishedChanged: 6, text: 'Two', textChanged: 6 }\n  })\n\n  expect(\n    await client2.collect(async () => {\n      await client1.process(\n        createTask({ fields: { finished: false, text: 'Four' }, id: '4' })\n      )\n    })\n  ).toEqual([\n    createdTask({ fields: { finished: false, text: 'Four' }, id: '4' })\n  ])\n\n  expect(\n    await client2.collect(async () => {\n      await client1.process(\n        createTask({ fields: { finished: true, text: 'Five' }, id: '5' })\n      )\n    })\n  ).toEqual([])\n\n  expect(\n    await client2.collect(async () => {\n      await client1.process(\n        createTask({ fields: { finished: true, text: 'S' }, id: 'silence' })\n      )\n    })\n  ).toEqual([])\n\n  let client3 = await server.connect('3')\n  expect(\n    await client3.subscribe('tasks', undefined, { id: '', time: 15 })\n  ).toEqual([\n    loguxSubscribed({ channel: 'tasks/1' }),\n    loguxSubscribed({ channel: 'tasks/2' }),\n    loguxSubscribed({ channel: 'tasks/4' }),\n    loguxSubscribed({ channel: 'tasks/5' }),\n    loguxSubscribed({ channel: 'tasks/silence' }),\n    changedTask({ fields: { text: 'One1' }, id: '1' }),\n    changedTask({ fields: { finished: false, text: 'Four' }, id: '4' }),\n    changedTask({ fields: { finished: true, text: 'Five' }, id: '5' }),\n    changedTask({ fields: { finished: true, text: 'S' }, id: 'silence' })\n  ])\n\n  expect(\n    await client3.collect(async () => {\n      await client1.process(\n        createTask({ fields: { finished: true, text: 'Six' }, id: '6' })\n      )\n    })\n  ).toEqual([createdTask({ fields: { finished: true, text: 'Six' }, id: '6' })])\n})\n\nit('supports simpler SyncMap', async () => {\n  let server = getServer()\n  addSyncMap<CommentValue>(server, 'comments', {\n    access() {\n      return true\n    },\n    load(ctx, id, since) {\n      if (since) {\n        return {\n          author: NoConflictResolution('A'),\n          id,\n          text: NoConflictResolution('updated')\n        }\n      }\n      return {\n        author: NoConflictResolution('A'),\n        id,\n        text: NoConflictResolution('full')\n      }\n    }\n  })\n  addSyncMapFilter<CommentValue>(server, 'comments', {\n    access() {\n      return true\n    },\n    initial() {\n      return []\n    }\n  })\n\n  let client1 = await server.connect('1')\n\n  expect(await client1.subscribe('comments/1')).toEqual([\n    changedComment({ fields: { author: 'A', text: 'full' }, id: '1' })\n  ])\n  expect(\n    await client1.subscribe('comments/2', undefined, { id: '', time: 2 })\n  ).toEqual([\n    changedComment({ fields: { author: 'A', text: 'updated' }, id: '2' })\n  ])\n\n  let client2 = await server.connect('2')\n  await client2.subscribe('comments')\n  await client2.collect(() =>\n    server.process(\n      changedComment({ fields: { author: 'A', text: '2' }, id: '10' })\n    )\n  )\n})\n\nit('allows to disable changes', async () => {\n  let server = getServer()\n  addSyncMap<CommentValue>(server, 'comments', {\n    access() {\n      return true\n    },\n    change(ctx, id) {\n      return id !== 'bad'\n    },\n    create(ctx, id) {\n      return id !== 'bad'\n    },\n    delete(ctx, id) {\n      return id !== 'bad'\n    },\n    load(ctx, id) {\n      return { id }\n    }\n  })\n  addSyncMapFilter<CommentValue>(server, 'comments', {\n    access() {\n      return true\n    },\n    initial() {\n      return []\n    }\n  })\n\n  let client1 = await server.connect('1')\n  let client2 = await server.connect('2')\n\n  await client2.subscribe('comments')\n  await client2.subscribe('comments/good')\n  await client2.subscribe('comments/bad')\n  expect(\n    await client2.collect(async () => {\n      await client1.process(createComment({ fields: {}, id: 'good' }))\n      await client1.process(changeComment({ fields: {}, id: 'good' }))\n      await client1.process(deleteComment({ id: 'good' }))\n      await client1.process(createComment({ fields: {}, id: 'bad' }))\n      await client1.process(changeComment({ fields: {}, id: 'bad' }))\n      await client1.process(deleteComment({ id: 'bad' }))\n    })\n  ).toEqual([\n    createdComment({ fields: {}, id: 'good' }),\n    changedComment({ fields: {}, id: 'good' }),\n    deletedComment({ id: 'good' })\n  ])\n})\n\nit('does not load data on creating', async () => {\n  let loaded = 0\n  let server = getServer()\n  addSyncMap<CommentValue>(server, 'comments', {\n    access() {\n      return true\n    },\n    load(ctx, id) {\n      loaded += 1\n      return { id }\n    }\n  })\n\n  let client = await server.connect('1')\n\n  await client.log.add({\n    channel: 'comments/new',\n    type: 'logux/subscribe'\n  })\n  await setTimeout(10)\n  expect(loaded).toBe(1)\n\n  await client.log.add({\n    channel: 'comments/new',\n    creating: true,\n    type: 'logux/subscribe'\n  })\n  await setTimeout(10)\n  expect(loaded).toBe(1)\n})\n\nit('throws an error on missed value wrapper', async () => {\n  let server = getServer()\n  addSyncMap<CommentValue>(server, 'comments', {\n    access() {\n      return true\n    },\n    // @ts-expect-error\n    load(ctx, id) {\n      return { id, text: 'Text' }\n    }\n  })\n\n  let client = await server.connect('1')\n\n  await server.expectError(/Wrap value/, () => client.subscribe('comments/1'))\n})\n"
  },
  {
    "path": "allowed-meta/index.d.ts",
    "content": "/**\n * List of meta keys permitted for clients.\n *\n *```js\n * import { ALLOWED_META } from '@logux/server'\n * async function onSend (action, meta) {\n *   const filtered = { }\n *   for (const i in meta) {\n *     if (ALLOWED_META.includes(i)) {\n *       filtered[i] = meta[i]\n *     }\n *   }\n *   return [action, filtered]\n * }\n * ```\n */\nexport const ALLOWED_META: string[]\n"
  },
  {
    "path": "allowed-meta/index.js",
    "content": "export const ALLOWED_META = ['id', 'time', 'subprotocol']\n"
  },
  {
    "path": "allowed-meta/index.test.ts",
    "content": "import { expect, it } from 'vitest'\n\nimport { ALLOWED_META } from '../index.js'\n\nit('has allowed meta keys list', () => {\n  for (let key of ALLOWED_META) {\n    expect(typeof key).toEqual('string')\n  }\n})\n"
  },
  {
    "path": "base-server/index.d.ts",
    "content": "import type {\n  AbstractActionCreator,\n  LoguxSubscribeAction,\n  LoguxUnsubscribeAction\n} from '@logux/actions'\nimport type {\n  Action,\n  AnyAction,\n  ID,\n  Log,\n  LogStore,\n  Meta,\n  ServerConnection,\n  TestTime\n} from '@logux/core'\nimport type { Unsubscribe } from 'nanoevents'\nimport type {\n  Server as HTTPServer,\n  IncomingMessage,\n  ServerResponse\n} from 'node:http'\nimport type { WebSocket } from 'ws'\n\nimport type {\n  ChannelContext,\n  ConnectContext,\n  Context\n} from '../context/index.js'\nimport type { ServerClient } from '../server-client/index.js'\n\ninterface LogFn {\n  (...objs: unknown[]): void\n}\n\ninterface TypeOptions {\n  /**\n   * Name of the queue that will be used to process actions\n   * of the specified type. Default is 'main'\n   */\n  queue?: string\n}\n\ninterface ChannelOptions {\n  /**\n   * Name of the queue that will be used to process channels\n   * with the specified name pattern. Default is 'main'\n   */\n  queue?: string\n}\n\ninterface ConnectLoader<Headers extends object = unknown> {\n  (\n    ctx: ConnectContext<Headers>,\n    lastSynced: number\n  ):\n    | [Action, ServerMeta][]\n    | Promise<\n        [\n          Action,\n          Partial<Pick<ServerMeta, 'subprotocol'>> &\n            Pick<ServerMeta, 'id' | 'time'>\n        ][]\n      >\n}\n\ntype ServerNodeConstructor = new (...args: unknown[]) => ServerNode\n\nexport interface ServerMeta extends Meta {\n  /**\n   * All nodes subscribed to channel will receive the action.\n   */\n  channel?: string\n\n  /**\n   * All nodes subscribed to listed channels will receive the action.\n   */\n  channels?: string[]\n\n  /**\n   * All nodes with listed client ID will receive the action.\n   */\n  client?: string\n\n  /**\n   * All nodes with listed client IDs will receive the action.\n   */\n  clients?: string[]\n\n  /**\n   * Client IDs, which will not receive the action.\n   */\n  excludeClients?: string[]\n\n  /**\n   * Node with listed node ID will receive the action.\n   */\n  node?: string\n\n  /**\n   * All nodes with listed node IDs will receive the action.\n   */\n  nodes?: string[]\n\n  /**\n   * Node ID of the server received the action.\n   */\n  server: string\n\n  /**\n   * Action processing status\n   */\n  status?: 'error' | 'processed' | 'waiting'\n\n  /**\n   * All nodes with listed user ID will receive the action.\n   */\n  user?: string\n\n  /**\n   * All nodes with listed user IDs will receive the action.\n   */\n  users?: string[]\n}\n\nexport interface BaseServerOptions {\n  /**\n   * SSL certificate or path to it. Path could be relative from server\n   * root. It is required in production mode, because WSS is highly\n   * recommended.\n   */\n  cert?: string\n\n  /**\n   * Regular expression which should be cleaned from error message and stack.\n   *\n   * By default it cleans `Bearer [^\\s\"]+`.\n   */\n  cleanFromLog?: RegExp\n\n  /**\n   * Disable health check endpoint, {@link Server#http}.\n   *\n   * The server will process only WebSocket connection and ignore all other\n   * HTTP request (so they can be processed by other HTTP server).\n   */\n  disableHttpServer?: boolean\n\n  /**\n   * Development or production server mode. By default,\n   * it will be taken from `NODE_ENV` environment variable.\n   * On empty `NODE_ENV` it will be `'development'`.\n   */\n  env?: 'development' | 'production'\n\n  /**\n   * URL of main JS file in the root dir for the cases where you can’t use\n   * `import.meta.dirname`.\n   *\n   * ```\n   * fileUrl: import.meta.url\n   * ```\n   */\n  fileUrl?: string\n\n  /**\n   * IP-address to bind server. Default is `127.0.0.1`.\n   */\n  host?: string\n\n  /**\n   * Custom random ID to be used in node ID.\n   */\n  id?: string\n\n  /**\n   * SSL key or path to it. Path could be relative from server root.\n   * It is required in production mode, because WSS is highly recommended.\n   */\n  key?: { pem: string } | string\n\n  /**\n   * The version requirements for client subprotocol version.\n   */\n  minSubprotocol?: number\n\n  /**\n   * Replace class for ServerNode.\n   */\n  Node?: ServerNodeConstructor\n\n  /**\n   * Process ID, to display in logs.\n   */\n  pid?: number\n\n  /**\n   * Milliseconds since last message to test connection by sending ping.\n   * Default is `20000`.\n   */\n  ping?: number\n\n  /**\n   * Port to bind server. It will create HTTP server manually to connect\n   * WebSocket server to it. Default is `31337`.\n   */\n  port?: number | string\n\n  /**\n   * URL to Redis for Logux Server Pro scaling.\n   */\n  redis?: string\n\n  /**\n   * Application root to load files and show errors.\n   * Default is `process.cwd()`.\n   *\n   * ```js\n   * root: import.meta.dirname\n   * ```\n   */\n  root?: string\n\n  /**\n   * HTTP server to serve Logux’s WebSocket and HTTP requests.\n   *\n   * Logux will remove previous HTTP callbacks. Do not use it with Express.js\n   * or other HTTP servers with defined routes.\n   */\n  server?: HTTPServer\n\n  /**\n   * Store to save log. Will be {@link @logux/core:MemoryStore}, by default.\n   */\n  store?: LogStore\n\n  /**\n   * Server current application subprotocol version.\n   */\n  subprotocol?: number\n\n  /**\n   * Test time to test server.\n   */\n  time?: TestTime\n\n  /**\n   * Timeout in milliseconds to disconnect connection.\n   * Default is `70000`.\n   */\n  timeout?: number\n}\n\nexport interface AuthenticatorOptions<Headers extends object> {\n  client: ServerClient\n  cookie: Record<string, string>\n  headers: Headers\n  token: string\n  userId: string\n}\n\nexport type SendBackActions =\n  | [Action, Partial<Meta>][]\n  | Action\n  | Action[]\n  | void\n\n/**\n * The authentication callback.\n *\n * @param userId User ID.\n * @param token The client credentials.\n * @param client Client object.\n * @returns `true` if credentials was correct\n */\ninterface ServerAuthenticator<Headers extends object> {\n  (user: AuthenticatorOptions<Headers>): boolean | Promise<boolean>\n}\n\n/**\n * Check does user can do this action.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns `true` if client are allowed to use this action.\n */\ninterface Authorizer<\n  TypeAction extends Action,\n  Data extends object,\n  Headers extends object\n> {\n  (\n    ctx: Context<Data, Headers>,\n    action: Readonly<TypeAction>,\n    meta: Readonly<ServerMeta>\n  ): boolean | Promise<boolean>\n}\n\n/**\n * Return object with keys for meta to resend action to other users.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns Meta’s keys.\n */\ninterface Resender<\n  TypeAction extends Action,\n  Data extends object,\n  Headers extends object\n> {\n  (\n    ctx: Context<Data, Headers>,\n    action: Readonly<TypeAction>,\n    meta: Readonly<ServerMeta>\n  ): Promise<Resend> | Resend\n}\n\n/**\n * Action business logic.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns Promise when processing will be finished.\n */\ninterface Processor<\n  TypeAction extends Action,\n  Data extends object,\n  Headers extends object\n> {\n  (\n    ctx: Context<Data, Headers>,\n    action: Readonly<TypeAction>,\n    meta: Readonly<ServerMeta>\n  ): Promise<void> | void\n}\n\n/**\n * Callback which will be run on the end of action/subscription\n * processing or on an error.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n */\ninterface ActionFinally<\n  TypeAction extends Action,\n  Data extends object,\n  Headers extends object\n> {\n  (\n    ctx: Context<Data, Headers>,\n    action: Readonly<TypeAction>,\n    meta: Readonly<ServerMeta>\n  ): void\n}\n\n/**\n * Channel filter callback\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns Should action be sent to client.\n */\ninterface ChannelFilter<Headers extends object> {\n  (\n    ctx: Context<unknown, Headers>,\n    action: Readonly<Action>,\n    meta: Readonly<ServerMeta>\n  ): boolean | Promise<boolean>\n}\n\n/**\n * Channel authorizer callback\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns `true` if client are allowed to subscribe to this channel.\n */\ninterface ChannelAuthorizer<\n  SubscribeAction extends Action,\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> {\n  (\n    ctx: ChannelContext<Data, ChannelParams, Headers>,\n    action: Readonly<SubscribeAction>,\n    meta: Readonly<ServerMeta>\n  ): boolean | Promise<boolean>\n}\n\n/**\n * Generates custom filter for channel’s actions.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns Actions filter.\n */\ninterface FilterCreator<\n  SubscribeAction extends Action,\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> {\n  (\n    ctx: ChannelContext<Data, ChannelParams, Headers>,\n    action: Readonly<SubscribeAction>,\n    meta: Readonly<ServerMeta>\n  ): ChannelFilter<Headers> | Promise<ChannelFilter<Headers>> | void\n}\n\n/**\n * Send actions with current state.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n * @returns Promise during current actions loading.\n */\ninterface ChannelLoader<\n  SubscribeAction extends Action,\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> {\n  (\n    ctx: ChannelContext<Data, ChannelParams, Headers>,\n    action: Readonly<SubscribeAction>,\n    meta: Readonly<ServerMeta>\n  ): Promise<SendBackActions> | SendBackActions\n}\n\n/**\n * Callback which will be run on the end of subscription\n * processing or on an error.\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n */\ninterface ChannelFinally<\n  SubscribeAction extends Action,\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> {\n  (\n    ctx: ChannelContext<Data, ChannelParams, Headers>,\n    action: Readonly<SubscribeAction>,\n    meta: Readonly<ServerMeta>\n  ): void\n}\n\n/**\n * Callback which will be called on listener unsubscribe\n * (with explicit intent or because of disconnect)\n *\n * @param ctx Information about node, who create this action.\n * @param action The action data.\n * @param meta The action metadata.\n */\ninterface ChannelUnsubscribe<\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> {\n  (\n    ctx: ChannelContext<Data, ChannelParams, Headers>,\n    action: LoguxUnsubscribeAction,\n    meta: Readonly<ServerMeta>\n  ): void\n}\n\ntype ActionCallbacks<\n  TypeAction extends Action,\n  Data extends object,\n  Headers extends object\n> = (\n  | {\n      access: Authorizer<TypeAction, Data, Headers>\n      process?: Processor<TypeAction, Data, Headers>\n    }\n  | {\n      accessAndProcess: Processor<TypeAction, Data, Headers>\n    }\n) & {\n  finally?: ActionFinally<TypeAction, Data, Headers>\n  resend?: Resender<TypeAction, Data, Headers>\n}\n\ntype ChannelCallbacks<\n  SubscribeAction extends Action,\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> = (\n  | {\n      access: ChannelAuthorizer<SubscribeAction, Data, ChannelParams, Headers>\n      load?: ChannelLoader<SubscribeAction, Data, ChannelParams, Headers>\n    }\n  | {\n      accessAndLoad: ChannelLoader<\n        SubscribeAction,\n        Data,\n        ChannelParams,\n        Headers\n      >\n    }\n) & {\n  filter?: FilterCreator<SubscribeAction, Data, ChannelParams, Headers>\n  finally?: ChannelFinally<SubscribeAction, Data, ChannelParams, Headers>\n  unsubscribe?: ChannelUnsubscribe<Data, ChannelParams, Headers>\n}\n\ninterface ActionReporter {\n  action: Readonly<Action>\n  meta: Readonly<ServerMeta>\n}\n\ninterface SubscriptionReporter {\n  actionId: ID\n  channel: string\n}\n\ninterface CleanReporter {\n  actionId: ID\n}\n\ninterface AuthenticationReporter {\n  connectionId: string\n  nodeId: string\n  subprotocol: string\n}\n\ninterface ReportersArguments {\n  add: ActionReporter\n  addClean: ActionReporter\n  authenticated: AuthenticationReporter\n  clean: CleanReporter\n  clientError: {\n    connectionId?: string\n    err: Error\n    nodeId?: string\n  }\n  connect: {\n    connectionId: string\n    ipAddress: string\n  }\n  denied: CleanReporter\n  destroy: void\n  disconnect: {\n    connectionId?: string\n    nodeId?: string\n  }\n  error: {\n    actionId?: ID\n    connectionId?: string\n    err: Error\n    fatal?: true\n    nodeId?: string\n  }\n  listen: {\n    cert: boolean\n    environment: 'development' | 'production'\n    host: string\n    loguxServer: string\n    minSubprotocol: number\n    nodeId: string\n    notes: object\n    port: string\n    redis: string\n    server: boolean\n    subprotocol: number\n  }\n  processed: {\n    actionId: ID\n    latency: number\n  }\n  subscribed: SubscriptionReporter\n  unauthenticated: AuthenticationReporter\n  unknownType: {\n    actionId: ID\n    type: string\n  }\n  unsubscribed: SubscriptionReporter\n  useless: ActionReporter\n  wrongChannel: SubscriptionReporter\n  zombie: {\n    nodeId: string\n  }\n}\n\nexport interface Reporter {\n  <Event extends keyof ReportersArguments>(\n    event: Event,\n    payload: ReportersArguments[Event]\n  ): void\n}\n\nexport type Resend =\n  | {\n      channel?: string\n      channels?: string[]\n      client?: string\n      clients?: string[]\n      excludeClients?: string[]\n      node?: string\n      nodes?: string[]\n      user?: string\n      users?: string[]\n    }\n  | string\n  | string[]\n\nexport interface Logger {\n  debug(details: object, message: string): void\n  error(details: object, message: string): void\n  fatal(details: object, message: string): void\n  info(details: object, message: string): void\n  warn(details: object, message: string): void\n}\n\n/**\n * Return `false` if `cb()` got response error with 403.\n *\n * ```js\n * import { wasNot403 } from '@logux/server'\n *\n * server.auth(({ userId, token }) => {\n *   return wasNot403(async () => {\n *     get(`/checkUser/${userId}/${token}`)\n *   })\n * })\n * ```\n *\n * @param cb Callback with `request` calls.\n */\nexport function wasNot403(cb: () => Promise<void>): Promise<boolean>\n\n/**\n * Base server class to extend.\n */\nexport class BaseServer<\n  Headers extends object = unknown,\n  ServerLog extends Log = Log<ServerMeta>\n> {\n  /**\n   * Connected client by client ID.\n   *\n   * Do not rely on this data, when you have multiple Logux servers.\n   * Each server will have a different list.\n   */\n  clientIds: Map<string, ServerClient>\n\n  /**\n   * Connected clients.\n   *\n   * ```js\n   * for (let client of server.connected.values()) {\n   *   console.log(client.remoteAddress)\n   * }\n   * ```\n   */\n  connected: Map<string, ServerClient>\n\n  /**\n   * Production or development mode.\n   *\n   * ```js\n   * if (server.env === 'development') {\n   *   logDebugData()\n   * }\n   * ```\n   */\n  env: 'development' | 'production'\n\n  /**\n   * Server actions log.\n   *\n   * ```js\n   * server.log.each(finder)\n   * ```\n   */\n  log: ServerLog\n\n  /**\n   * Console for custom log records. It uses `pino` API.\n   *\n   * ```js\n   * server.on('connected', client => {\n   *   server.logger.info(\n   *     { domain: client.httpHeaders.domain },\n   *     'Client domain'\n   *   )\n   * })\n   * ```\n   */\n  logger: {\n    debug: LogFn\n    error: LogFn\n    fatal: LogFn\n    info: LogFn\n    warn: LogFn\n  }\n\n  /**\n   * Server unique ID.\n   *\n   * ```js\n   * console.log('Error was raised on ' + server.nodeId)\n   * ```\n   */\n  nodeId: string\n\n  /**\n   * Connected client by node ID.\n   *\n   * Do not rely on this data, when you have multiple Logux servers.\n   * Each server will have a different list.\n   */\n  nodeIds: Map<string, ServerClient>\n\n  /**\n   * Server options.\n   *\n   * ```js\n   * console.log('Server options', server.options.subprotocol)\n   * ```\n   */\n  options: BaseServerOptions\n\n  /**\n   * Clients subscribed to some channel.\n   *\n   * Do not rely on this data, when you have multiple Logux servers.\n   * Each server will have a different list.\n   */\n  subscribers: {\n    [channel: string]: {\n      [nodeId: string]: {\n        filters: Record<string, ChannelFilter<unknown> | true>\n        unsubscribe?: (action: LoguxUnsubscribeAction, meta: ServerMeta) => void\n      }\n    }\n  }\n\n  /**\n   * Connected client by user ID.\n   *\n   * Do not rely on this data, when you have multiple Logux servers.\n   * Each server will have a different list.\n   */\n  userIds: Map<string, ServerClient[]>\n\n  /**\n   * @param opts Server options.\n   */\n  constructor(opts: BaseServerOptions)\n\n  /**\n   * Add new client for server. You should call this method manually\n   * mostly for test purposes.\n   *\n   * ```js\n   * server.addClient(test.right)\n   * ```\n   *\n   * @param connection Logux connection to client.\n   * @returns Client ID.\n   */\n  addClient(connection: ServerConnection): number\n\n  /**\n   * Set authenticate function. It will receive client credentials\n   * and node ID. It should return a Promise with `true` or `false`.\n   *\n   * ```js\n   * server.auth(async ({ userId, cookie }) => {\n   *   const user = await findUserByToken(cookie.token)\n   *   return !!user && userId === user.id\n   * })\n   * ```\n   *\n   * @param authenticator The authentication callback.\n   */\n  auth(authenticator: ServerAuthenticator<Headers>): void\n\n  /**\n   * Define the channel.\n   *\n   * ```js\n   * server.channel('user/:id', {\n   *   access (ctx, action, meta) {\n   *     return ctx.params.id === ctx.userId\n   *   }\n   *   filter (ctx, action, meta) {\n   *     return (otherCtx, otherAction, otherMeta) => {\n   *       return !action.hidden\n   *     }\n   *   }\n   *   async load (ctx, action, meta) {\n   *     const user = await db.loadUser(ctx.params.id)\n   *     ctx.sendBack({ type: 'USER_NAME', name: user.name })\n   *   }\n   * })\n   * ```\n   *\n   * @param pattern Pattern for channel name.\n   * @param callbacks Callback during subscription process.\n   * @param options Additional options\n   */\n  channel<\n    ChannelParams extends object = unknown,\n    Data extends object = unknown,\n    SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction\n  >(\n    pattern: string,\n    callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>,\n    options?: ChannelOptions\n  ): void\n  /**\n   * @param pattern Regular expression for channel name.\n   * @param callbacks Callback during subscription process.\n   * @param options Additional options\n   */\n  channel<\n    ChannelParams extends string[] = string[],\n    Data extends object = unknown,\n    SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction\n  >(\n    pattern: RegExp,\n    callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>,\n    options?: ChannelOptions\n  ): void\n\n  /**\n   * Send runtime error stacktrace to all clients.\n   *\n   * ```js\n   * process.on('uncaughtException', e => {\n   *   server.debugError(e)\n   * })\n   * ```\n   *\n   * @param error Runtime error instance.\n   */\n  debugError(error: Error): void\n\n  /**\n   * Stop server and unbind all listeners.\n   *\n   * ```js\n   * afterEach(() => {\n   *   testServer.destroy()\n   * })\n   * ```\n   *\n   * @returns Promise when all listeners will be removed.\n   */\n  destroy(): Promise<void>\n\n  /**\n   * Handle WebSocket connection explicitly\n   *\n   * This is a low-level method allowing to integrate Logux server with an existing server\n   *\n   * ```js\n   * fastify.get('/', { websocket: true }, (socket, req) => {\n   *   loguxServer.handleClient(socket, req)\n   * })\n   * ```\n   */\n  handleClient(ws: WebSocket, req: IncomingMessage): void\n\n  /**\n   * Add non-WebSocket HTTP request processor.\n   *\n   * ```js\n   * server.http('GET', '/auth', (req, res) => {\n   *   let token = signIn(req)\n   *   if (token) {\n   *     res.setHeader('Set-Cookie', `token=${token}; Secure; HttpOnly`)\n   *     res.end()\n   *   } else {\n   *     res.statusCode = 400\n   *     res.end('Wrong user or password')\n   *   }\n   * })\n   * ```\n   */\n  http(\n    method: string,\n    url: string,\n    listener: (\n      req: IncomingMessage,\n      res: ServerResponse\n    ) => Promise<void> | void\n  ): void\n  http(\n    listener: (\n      req: IncomingMessage,\n      res: ServerResponse\n    ) => boolean | Promise<boolean>\n  ): void\n\n  /**\n   * Start WebSocket server and listen for clients.\n   *\n   * @returns When the server has been bound.\n   */\n  listen(): Promise<void>\n\n  /**\n   * @param event The event name.\n   * @param listener Event listener.\n   */\n  on(event: 'subscriptionCancelled', listener: () => void): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Subscription listener.\n   */\n  on(\n    event: 'subscribing',\n    listener: (action: LoguxSubscribeAction, meta: Readonly<ServerMeta>) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Processing listener.\n   */\n  on(\n    event: 'processed',\n    listener: (\n      action: Action,\n      meta: Readonly<ServerMeta>,\n      latencyMilliseconds: number\n    ) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Action listener.\n   */\n  on(\n    event: 'add' | 'clean',\n    listener: (action: Action, meta: Readonly<ServerMeta>) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Client listener.\n   */\n  on(\n    event: 'connected' | 'disconnected',\n    listener: (client: ServerClient) => void\n  ): Unsubscribe\n  /**\n   * Subscribe for synchronization events. It implements nanoevents API.\n   * Supported events:\n   *\n   * * `error`: server error during action processing.\n   * * `fatal`: server error during loading.\n   * * `clientError`: wrong client behaviour.\n   * * `connected`: new client was connected.\n   * * `disconnected`: client was disconnected.\n   * * `authenticated`: client was authenticated.\n   * * `preadd`: action is going to be added to the log.\n   *   The best place to set `reasons`.\n   * * `add`: action was added to the log.\n   * * `clean`: action was cleaned from the log.\n   * * `processed`: action processing was finished.\n   * * `subscribed`: channel initial data was loaded.\n   * * `subscribing`: channel initial data started to be loaded.\n   * * `unsubscribed`: node was unsubscribed.\n   * * `subscriptionCancelled`: subscription was cancelled because the client\n   *    is not connected.\n   *\n   * ```js\n   * server.on('error', error => {\n   *   trackError(error)\n   * })\n   * ```\n   *\n   * @param event The event name.\n   * @param listener The listener function.\n   * @returns Unbind listener from event.\n   */\n  on(\n    event: 'clientError' | 'fatal',\n    listener: (err: Error) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Error listener.\n   */\n  on(\n    event: 'error',\n    listener: (err: Error, action: Action, meta: Readonly<ServerMeta>) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Client listener.\n   */\n  on(\n    event: 'authenticated' | 'unauthenticated',\n    listener: (client: ServerClient, latencyMilliseconds: number) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Action listener.\n   */\n  on(\n    event: 'preadd',\n    listener: (action: Action, meta: ServerMeta) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Subscription listener.\n   */\n  on(\n    event: 'subscribed',\n    listener: (\n      action: LoguxSubscribeAction,\n      meta: Readonly<ServerMeta>,\n      latencyMilliseconds: number\n    ) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Subscription listener.\n   */\n  on(\n    event: 'unsubscribed',\n    listener: (\n      action: LoguxUnsubscribeAction,\n      meta: Readonly<ServerMeta>,\n      clientNodeId: string\n    ) => void\n  ): Unsubscribe\n  /**\n   * @param event The event name.\n   * @param listener Report listener.\n   */\n  on(event: 'report', listener: Reporter): Unsubscribe\n\n  /**\n   * Set callbacks for unknown channel subscription.\n   *\n   *```js\n   * server.otherChannel({\n   *   async access (ctx, action, meta) {\n   *     const res = await phpBackend.checkChannel(ctx.params[0], ctx.userId)\n   *     if (res.code === 404) {\n   *       this.wrongChannel(action, meta)\n   *       return false\n   *     } else {\n   *       return response.body === 'granted'\n   *     }\n   *   }\n   * })\n   * ```\n   *\n   * @param callbacks Callback during subscription process.\n   */\n  otherChannel<Data extends object = unknown>(\n    callbacks: ChannelCallbacks<LoguxSubscribeAction, Data, [string], Headers>\n  ): void\n\n  /**\n   * Define callbacks for actions, which type was not defined\n   * by any {@link Server#type}. Useful for proxy or some hacks.\n   *\n   * Without this settings, server will call {@link Server#unknownType}\n   * on unknown type.\n   *\n   * ```js\n   * server.otherType(\n   *   async access (ctx, action, meta) {\n   *     const response = await phpBackend.checkByHTTP(action, meta)\n   *     if (response.code === 404) {\n   *       this.unknownType(action, meta)\n   *       return false\n   *     } else {\n   *       return response.body === 'granted'\n   *     }\n   *   }\n   *   async process (ctx, action, meta) {\n   *     return await phpBackend.sendHTTP(action, meta)\n   *   }\n   * })\n   * ```\n   *\n   * @param callbacks Callbacks for actions with this type.\n   */\n  otherType<Data extends object = unknown>(\n    callbacks: ActionCallbacks<Action, Data, Headers>\n  ): void\n\n  /**\n   * Add new action to the server and return the Promise until it will be\n   * resend to clients and processed.\n   *\n   * @param action New action to resend and process.\n   * @param meta Action’s meta.\n   * @returns Promise until new action will be resend to clients and processed.\n   */\n  process(\n    action: AnyAction,\n    meta?: Partial<ServerMeta>\n  ): Promise<Readonly<ServerMeta>>\n\n  /**\n   * Send action, received by other server, to all clients of current server.\n   * This method is for multi-server configuration only.\n   *\n   * ```js\n   * server.on('add', (action, meta) => {\n   *   if (meta.server === server.nodeId) {\n   *     sendToOtherServers(action, meta)\n   *   }\n   * })\n   * onReceivingFromOtherServer((action, meta) => {\n   *   server.sendAction(action, meta)\n   * })\n   * ```\n   *\n   * @param action New action.\n   * @param meta Action’s metadata.\n   */\n  sendAction(action: Action, meta: ServerMeta): Promise<void> | void\n\n  /**\n   * Change a way how server loads actions history for the client.\n   *\n   * ```js\n   * server.sendOnConnect(async (ctx, lastSynced) => {\n   *   return db.loadActions({ user: ctx.userId, after: lastSynced })\n   * })\n   * ```\n   *\n   * @param loader Callback which loads list of actions and meta.\n   */\n  sendOnConnect(loader: ConnectLoader<Headers>): void\n\n  /**\n   * Send `logux/subscribed` if client was not already subscribed.\n   *\n   * ```js\n   * server.subscribe(ctx.nodeId, `users/${loaded}`)\n   * ```\n   *\n   * @param nodeId Node ID.\n   * @param channel Channel name.\n   */\n  subscribe(nodeId: string, channel: string): void\n\n  /**\n   * @param actionCreator Action creator function.\n   * @param callbacks Callbacks for action created by creator.\n   * @param options Additional options\n   */\n  type<Creator extends AbstractActionCreator, Data extends object = unknown>(\n    actionCreator: Creator,\n    callbacks: ActionCallbacks<ReturnType<Creator>, Data, Headers>,\n    options?: TypeOptions\n  ): void\n  /**\n   * Define action type’s callbacks.\n   *\n   * ```js\n   * server.type('CHANGE_NAME', {\n   *   access (ctx, action, meta) {\n   *     return action.user === ctx.userId\n   *   },\n   *   resend (ctx, action) {\n   *     return `user/${ action.user }`\n   *   }\n   *   process (ctx, action, meta) {\n   *     if (isFirstOlder(lastNameChange(action.user), meta)) {\n   *       return db.changeUserName({ id: action.user, name: action.name })\n   *     }\n   *   }\n   * })\n   * ```\n   *\n   * @param name The action’s type or action’s type matching rule as RegExp..\n   * @param callbacks Callbacks for actions with this type.\n   * @param options Additional options\n   */\n  type<TypeAction extends Action = AnyAction, Data extends object = unknown>(\n    name: RegExp | TypeAction['type'],\n    callbacks: ActionCallbacks<TypeAction, Data, Headers>,\n    options?: TypeOptions\n  ): void\n\n  /**\n   * Undo action from client.\n   *\n   * ```js\n   * if (couldNotFixConflict(action, meta)) {\n   *   server.undo(action, meta)\n   * }\n   * ```\n   *\n   * @param action The original action to undo.\n   * @param meta The action’s metadata.\n   * @param reason Optional code for reason. Default is `'error'`.\n   * @param extra Extra fields to `logux/undo` action.\n   * @returns When action was saved to the log.\n   */\n  undo(\n    action: Action,\n    meta: ServerMeta,\n    reason?: string,\n    extra?: object\n  ): Promise<void>\n\n  /**\n   * If you receive action with unknown type, this method will mark this action\n   * with `error` status and undo it on the clients.\n   *\n   * If you didn’t set {@link Server#otherType},\n   * Logux will call it automatically.\n   *\n   * ```js\n   * server.otherType({\n   *   access (ctx, action, meta) {\n   *     if (action.type.startsWith('myapp/')) {\n   *       return proxy.access(action, meta)\n   *     } else {\n   *       server.unknownType(action, meta)\n   *     }\n   *   }\n   * })\n   * ```\n   *\n   * @param action The action with unknown type.\n   * @param meta Action’s metadata.\n   */\n  unknownType(action: Action, meta: ServerMeta): void\n\n  /**\n   * Report that client try to subscribe for unknown channel.\n   *\n   * Logux call it automatically,\n   * if you will not set {@link Server#otherChannel}.\n   *\n   * ```js\n   * server.otherChannel({\n   *   async access (ctx, action, meta) {\n   *     const res = phpBackend.checkChannel(params[0], ctx.userId)\n   *     if (res.code === 404) {\n   *       this.wrongChannel(action, meta)\n   *       return false\n   *     } else {\n   *       return response.body === 'granted'\n   *     }\n   *   }\n   * })\n   * ```\n   *\n   * @param action The subscribe action.\n   * @param meta Action’s metadata.\n   */\n  wrongChannel(action: LoguxSubscribeAction, meta: ServerMeta): void\n}\n"
  },
  {
    "path": "base-server/index.js",
    "content": "import { LoguxNotFoundError } from '@logux/actions'\nimport { Log, MemoryStore, parseId, ServerConnection } from '@logux/core'\nimport { createNanoEvents } from 'nanoevents'\nimport { nanoid } from 'nanoid'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport UrlPattern from 'url-pattern'\nimport { WebSocketServer } from 'ws'\n\nimport { addHttpPages } from '../add-http-pages/index.js'\nimport { Context } from '../context/index.js'\nimport { createHttpServer } from '../create-http-server/index.js'\nimport { ServerClient } from '../server-client/index.js'\n\nconst SKIP_PROCESS = Symbol('skipProcess')\nconst RESEND_META = ['channels', 'users', 'clients', 'nodes']\n\nfunction optionError(msg) {\n  let error = new Error(msg)\n  error.logux = true\n  error.note = 'Check server constructor and Logux Server documentation'\n  throw error\n}\n\nexport async function wasNot403(cb) {\n  try {\n    await cb()\n    return true\n  } catch (e) {\n    if (e.name === 'ResponseError' && e.statusCode === 403) {\n      return false\n    }\n    throw e\n  }\n}\n\nfunction normalizeTypeCallbacks(name, callbacks) {\n  if (callbacks && callbacks.accessAndProcess) {\n    callbacks.access = (ctx, ...args) => {\n      return wasNot403(async () => {\n        await callbacks.accessAndProcess(ctx, ...args)\n        ctx[SKIP_PROCESS] = true\n      })\n    }\n    callbacks.process = async (ctx, ...args) => {\n      if (!ctx[SKIP_PROCESS]) await callbacks.accessAndProcess(ctx, ...args)\n    }\n  }\n  if (!callbacks || !callbacks.access) {\n    throw new Error(`${name} must have access callback`)\n  }\n}\n\nfunction normalizeChannelCallbacks(pattern, callbacks) {\n  if (callbacks && callbacks.accessAndLoad) {\n    callbacks.access = (ctx, ...args) => {\n      return wasNot403(async () => {\n        try {\n          ctx.data.load = await callbacks.accessAndLoad(ctx, ...args)\n        } catch (e) {\n          if (e.name === 'LoguxNotFoundError') {\n            ctx.data.notFound = true\n          } else if (e.name === 'ResponseError' && e.statusCode === 404) {\n            ctx.data.notFound = true\n          } else {\n            throw e\n          }\n        }\n      })\n    }\n    callbacks.load = ctx => {\n      if (ctx.data.notFound) {\n        throw new LoguxNotFoundError()\n      } else {\n        return ctx.data.load\n      }\n    }\n  }\n  if (!callbacks || !callbacks.access) {\n    throw new Error(`Channel ${pattern} must have access callback`)\n  }\n}\n\nfunction subscriberFilterId(action) {\n  return JSON.stringify(action.filter || {})\n}\n\nexport class BaseServer {\n  constructor(opts = {}) {\n    this.options = opts\n    this.env = this.options.env || process.env.NODE_ENV || 'development'\n\n    if (typeof this.options.subprotocol === 'undefined') {\n      throw optionError('Missed `subprotocol` option in server constructor')\n    }\n    if (typeof this.options.minSubprotocol === 'undefined') {\n      throw optionError('Missed `minSubprotocol` option in server constructor')\n    }\n\n    if (this.options.key && !this.options.cert) {\n      throw optionError('You must set `cert` option if you use `key` option')\n    }\n    if (!this.options.key && this.options.cert) {\n      throw optionError('You must set `key` option if you use `cert` option')\n    }\n\n    if (!this.options.server) {\n      if (!this.options.port) this.options.port = 31337\n      if (!this.options.host) this.options.host = '127.0.0.1'\n    }\n\n    this.nodeId = `server:${this.options.id || nanoid(8)}`\n\n    if (this.options.fileUrl) {\n      this.options.root = dirname(fileURLToPath(this.options.fileUrl))\n    }\n\n    this.options.root = this.options.root || process.cwd()\n    if (typeof this.options.port === 'string') {\n      this.options.port = parseInt(this.options.port, 10)\n    }\n\n    let store = this.options.store || new MemoryStore()\n\n    let log\n    if (this.options.time) {\n      log = this.options.time.nextLog({ nodeId: this.nodeId, store })\n    } else {\n      log = new Log({ nodeId: this.nodeId, store })\n    }\n\n    this.logger = console\n\n    this.contexts = new WeakMap()\n    this.log = log\n\n    let cleaned = {}\n\n    this.on('preadd', (action, meta) => {\n      let isLogux = action.type.slice(0, 6) === 'logux/'\n      if (!meta.server) {\n        meta.server = this.nodeId\n      }\n      if (!meta.status && !isLogux) {\n        meta.status = 'waiting'\n      }\n      if (meta.id.split(' ')[1] === this.nodeId) {\n        if (!meta.subprotocol) {\n          meta.subprotocol = this.options.subprotocol\n        }\n        if (\n          !isLogux &&\n          !this.types[action.type] &&\n          !this.getRegexProcessor(action.type)\n        ) {\n          meta.status = 'processed'\n        }\n      }\n      this.replaceResendShortcuts(meta)\n    })\n    this.on('add', async (action, meta) => {\n      let start = Date.now()\n      if (meta.reasons.length === 0) {\n        cleaned[meta.id] = true\n        this.emitter.emit('report', 'addClean', { action, meta })\n      } else {\n        this.emitter.emit('report', 'add', { action, meta })\n      }\n\n      if (this.destroying && !this.actionToQueue.has(meta.id)) {\n        return\n      }\n\n      if (action.type === 'logux/subscribe') {\n        if (meta.server === this.nodeId) {\n          void this.subscribeAction(action, meta, start)\n        }\n        return\n      }\n\n      if (action.type === 'logux/unsubscribe') {\n        if (meta.server === this.nodeId) {\n          this.unsubscribeAction(action, meta)\n        }\n        return\n      }\n\n      let processor = this.getProcessor(action.type)\n      if (processor && processor.resend && meta.status === 'waiting') {\n        let ctx = this.createContext(action, meta)\n        let resend\n        try {\n          resend = await processor.resend(ctx, action, meta)\n        } catch (e) {\n          this.undo(action, meta, 'error')\n          this.emitter.emit('error', e, action, meta)\n          this.finally(processor, ctx, action, meta)\n          return\n        }\n        if (resend) {\n          if (typeof resend === 'string') {\n            resend = { channels: [resend] }\n          } else if (Array.isArray(resend)) {\n            resend = { channels: resend }\n          } else {\n            this.replaceResendShortcuts(resend)\n          }\n          let diff = {}\n          for (let i of RESEND_META) {\n            if (resend[i]) diff[i] = resend[i]\n          }\n          await this.log.changeMeta(meta.id, diff)\n          meta = { ...meta, ...diff }\n        }\n      }\n\n      if (this.isUseless(action, meta)) {\n        this.emitter.emit('report', 'useless', { action, meta })\n      }\n\n      await this.sendAction(action, meta)\n\n      if (meta.status === 'waiting') {\n        if (!processor) {\n          this.internalUnknownType(action, meta)\n          return\n        }\n        if (processor.process) {\n          void this.processAction(processor, action, meta, start)\n        } else {\n          this.emitter.emit('processed', action, meta, 0)\n          this.finally(\n            processor,\n            this.createContext(action, meta),\n            action,\n            meta\n          )\n          this.markAsProcessed(meta)\n        }\n      } else {\n        this.emitter.emit('processed', action, meta, 0)\n        this.finally(processor, this.createContext(action, meta), action, meta)\n      }\n    })\n    this.on('clean', (action, meta) => {\n      if (cleaned[meta.id]) {\n        delete cleaned[meta.id]\n        return\n      }\n      this.emitter.emit('report', 'clean', { actionId: meta.id })\n    })\n\n    this.emitter = createNanoEvents()\n    this.on('fatal', err => {\n      this.emitter.emit('report', 'error', { err, fatal: true })\n    })\n    this.on('error', (err, action, meta) => {\n      if (meta) {\n        this.emitter.emit('report', 'error', { actionId: meta.id, err })\n      } else if (err.nodeId) {\n        this.emitter.emit('report', 'error', { err, nodeId: err.nodeId })\n      } else if (err.connectionId) {\n        this.emitter.emit('report', 'error', {\n          connectionId: err.connectionId,\n          err\n        })\n      }\n      if (this.env === 'development') this.debugError(err)\n    })\n    this.on('clientError', err => {\n      if (err.nodeId) {\n        this.emitter.emit('report', 'clientError', { err, nodeId: err.nodeId })\n      } else if (err.connectionId) {\n        this.emitter.emit('report', 'clientError', {\n          connectionId: err.connectionId,\n          err\n        })\n      }\n    })\n    this.on('connected', client => {\n      this.emitter.emit('report', 'connect', {\n        connectionId: client.key,\n        ipAddress: client.remoteAddress\n      })\n    })\n    this.on('disconnected', client => {\n      if (!client.zombie) {\n        if (client.nodeId) {\n          this.emitter.emit('report', 'disconnect', { nodeId: client.nodeId })\n        } else {\n          this.emitter.emit('report', 'disconnect', {\n            connectionId: client.key\n          })\n        }\n      }\n    })\n\n    this.unbind = []\n\n    this.connected = new Map()\n    this.nodeIds = new Map()\n    this.clientIds = new Map()\n    this.userIds = new Map()\n    this.types = {}\n    this.regexTypes = new Map()\n    this.processing = 0\n\n    this.lastClient = 0\n\n    this.channels = []\n    this.subscribers = {}\n\n    this.authAttempts = {}\n    this.unknownTypes = {}\n    this.wrongChannels = {}\n\n    this.timeouts = {}\n    this.lastTimeout = 0\n\n    this.typeToQueue = new Map()\n    this.queues = new Map()\n    this.actionToQueue = new Map()\n\n    this.httpListeners = {}\n    this.httpAllListeners = []\n    addHttpPages(this)\n\n    this.listenNotes = {}\n\n    let end = (actionId, queue, queueKey, ...args) => {\n      this.actionToQueue.delete(actionId)\n      if (queue.length() === 0) {\n        this.queues.delete(queueKey)\n      }\n      queue.next(...args)\n    }\n    let undoRemainingTasks = queue => {\n      let remainingTasks = queue.getQueue()\n      if (remainingTasks) {\n        for (let task of remainingTasks) {\n          this.undo(task.action, task.meta, 'error')\n          this.actionToQueue.delete(task.meta.id)\n        }\n      }\n      queue.killAndDrain()\n    }\n    this.on('error', (e, action, meta) => {\n      let queueKey = this.actionToQueue.get(meta?.id)\n      if (queueKey) {\n        let queue = this.queues.get(queueKey)\n        undoRemainingTasks(queue)\n        end(meta.id, queue, queueKey, e)\n      }\n    })\n    this.on('processed', (action, meta) => {\n      if (action.type === 'logux/undo') {\n        let queueKey = this.actionToQueue.get(action.id)\n        if (queueKey) {\n          let queue = this.queues.get(queueKey)\n          undoRemainingTasks(queue)\n          end(action.id, queue, queueKey, null, meta)\n        }\n      } else if (action.type === 'logux/processed') {\n        let queueKey = this.actionToQueue.get(action.id)\n        if (queueKey) {\n          let queue = this.queues.get(queueKey)\n          end(action.id, queue, queueKey, null, meta)\n        }\n      } else if (\n        action.type !== 'logux/subscribed' &&\n        action.type !== 'logux/unsubscribed'\n      ) {\n        let queueKey = this.actionToQueue.get(meta.id)\n        if (queueKey) {\n          let queue = this.queues.get(queueKey)\n          end(meta.id, queue, queueKey, null, meta)\n        }\n      }\n    })\n\n    this.unbind.push(() => {\n      for (let i of this.connected.values()) i.destroy()\n      for (let i in this.timeouts) {\n        clearTimeout(this.timeouts[i])\n      }\n    })\n    this.unbind.push(() => {\n      return new Promise(resolve => {\n        if (this.processing === 0) {\n          resolve()\n        } else {\n          this.on('processed', () => {\n            if (this.processing === 0) resolve()\n          })\n        }\n      })\n    })\n    this.unbind.push(() => {\n      return Promise.allSettled(\n        [...this.queues.values()].map(queue => {\n          return new Promise(resolve => {\n            queue.drain = resolve\n          })\n        })\n      )\n    })\n  }\n\n  addClient(connection) {\n    this.lastClient += 1\n    let key = this.lastClient.toString()\n    let client = new ServerClient(this, connection, key)\n    this.connected.set(key, client)\n    return this.lastClient\n  }\n\n  auth(authenticator) {\n    this.authenticator = authenticator\n  }\n\n  buildUndo(action, meta, reason, extra) {\n    let undoMeta = { status: 'processed' }\n\n    if (meta.users) undoMeta.users = meta.users.slice(0)\n    if (meta.nodes) undoMeta.nodes = meta.nodes.slice(0)\n    if (meta.clients) undoMeta.clients = meta.clients.slice(0)\n    if (meta.reasons) undoMeta.reasons = meta.reasons.slice(0)\n    if (meta.channels) undoMeta.channels = meta.channels.slice(0)\n    if (meta.excludeClients) {\n      undoMeta.excludeClients = meta.excludeClients.slice(0)\n    }\n\n    let undoAction = {\n      ...extra,\n      action,\n      id: meta.id,\n      reason,\n      type: 'logux/undo'\n    }\n    return [undoAction, undoMeta]\n  }\n\n  channel(pattern, callbacks, options = {}) {\n    normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)\n    let channel = Object.assign({}, callbacks)\n    if (typeof pattern === 'string') {\n      channel.pattern = new UrlPattern(pattern, {\n        segmentValueCharset: '^/'\n      })\n    } else {\n      channel.regexp = pattern\n    }\n\n    channel.queueName = options.queue || 'main'\n    this.channels.push(channel)\n  }\n\n  createContext(action, meta) {\n    let context = this.contexts.get(action)\n    if (!context) {\n      context = new Context(this, meta)\n      this.contexts.set(action, context)\n    }\n    return context\n  }\n\n  debugActionError(meta, msg) {\n    if (this.env === 'development') {\n      let clientId = parseId(meta.id).clientId\n      if (this.clientIds.has(clientId)) {\n        this.clientIds.get(clientId).connection.send(['debug', 'error', msg])\n      }\n    }\n  }\n\n  debugError(error) {\n    for (let i of this.connected.values()) {\n      if (i.connection.connected) {\n        try {\n          i.connection.send(['debug', 'error', error.stack])\n        } catch {}\n      }\n    }\n  }\n\n  denyAction(action, meta) {\n    this.emitter.emit('report', 'denied', { actionId: meta.id })\n    this.undo(action, meta, 'denied')\n    this.debugActionError(meta, `Action \"${meta.id}\" was denied`)\n  }\n\n  destroy() {\n    this.destroying = true\n    this.emitter.emit('report', 'destroy')\n    return Promise.all(this.unbind.map(i => i()))\n  }\n\n  finally(processor, ctx, action, meta) {\n    this.contexts.delete(action)\n    if (processor && processor.finally) {\n      try {\n        processor.finally(ctx, action, meta)\n      } catch (err) {\n        this.emitter.emit('error', err, action, meta)\n      }\n    }\n  }\n\n  getProcessor(type) {\n    return (\n      this.types[type] || this.getRegexProcessor(type) || this.otherProcessor\n    )\n  }\n\n  getRegexProcessor(type) {\n    for (let regexp of this.regexTypes.keys()) {\n      if (type.match(regexp) !== null) {\n        return this.regexTypes.get(regexp)\n      }\n    }\n    return undefined\n  }\n\n  handleClient(ws, req) {\n    ws.upgradeReq = req\n    this.addClient(new ServerConnection(ws))\n  }\n\n  http(method, url, listener) {\n    if (this.options.disableHttpServer) {\n      throw new Error(\n        '`server.http()` can not be called when `disableHttpServer` enabled'\n      )\n    }\n    if (!url) {\n      this.httpAllListeners.push(method)\n    } else {\n      this.httpListeners[`${method} ${url}`] = listener\n    }\n  }\n\n  internalUnknownType(action, meta) {\n    this.contexts.delete(action)\n    this.log.changeMeta(meta.id, { status: 'error' })\n    this.emitter.emit('report', 'unknownType', {\n      actionId: meta.id,\n      type: action.type\n    })\n    if (parseId(meta.id).userId !== 'server') {\n      this.undo(action, meta, 'unknownType')\n    }\n    this.debugActionError(meta, `Action with unknown type ${action.type}`)\n  }\n\n  internalWrongChannel(action, meta) {\n    this.contexts.delete(action)\n    this.emitter.emit('report', 'wrongChannel', {\n      actionId: meta.id,\n      channel: action.channel\n    })\n    this.undo(action, meta, 'wrongChannel')\n    this.debugActionError(meta, `Wrong channel name ${action.channel}`)\n  }\n\n  isBruteforce(ip) {\n    let attempts = this.authAttempts[ip]\n    return attempts && attempts >= 3\n  }\n\n  isUseless(action, meta) {\n    if (\n      meta.status !== 'processed' ||\n      this.types[action.type] ||\n      this.getRegexProcessor(action.type)\n    ) {\n      return false\n    }\n    for (let i of ['channels', 'nodes', 'clients', 'users']) {\n      if (Array.isArray(meta[i]) && meta[i].length > 0) return false\n    }\n    return true\n  }\n\n  async listen() {\n    if (!this.authenticator) {\n      throw new Error('You must set authentication callback by server.auth()')\n    }\n    this.httpServer = await createHttpServer(this.options)\n    this.ws = new WebSocketServer({ server: this.httpServer })\n    if (!this.options.server) {\n      await new Promise((resolve, reject) => {\n        this.ws.on('error', reject)\n        this.httpServer.listen(this.options.port, this.options.host, resolve)\n      })\n    }\n\n    let processing = 0\n    let waiting\n    this.unbind.push(() => {\n      return new Promise(resolve => {\n        let end = () => {\n          this.ws.close(resolve)\n          this.httpServer.close()\n        }\n        if (processing === 0) {\n          end()\n        } else {\n          waiting = end\n        }\n      })\n    })\n\n    if (!this.options.disableHttpServer) {\n      this.httpServer.on('request', async (req, res) => {\n        if (this.destroying) {\n          res.writeHead(503, { 'Content-Type': 'text/plain' })\n          res.end('The server is shutting down\\n')\n          return\n        }\n        processing += 1\n        await this.processHttp(req, res)\n        processing -= 1\n        if (processing === 0 && waiting) waiting()\n      })\n    }\n\n    let pkg = JSON.parse(\n      await readFile(join(import.meta.dirname, '..', 'package.json'))\n    )\n\n    this.ws.on('connection', (ws, req) => this.handleClient(ws, req))\n    this.emitter.emit('report', 'listen', {\n      cert: !!this.options.cert,\n      environment: this.env,\n      host: this.options.host,\n      loguxServer: pkg.version,\n      minSubprotocol: this.options.minSubprotocol,\n      nodeId: this.nodeId,\n      notes: this.listenNotes,\n      port: this.options.port,\n      redis: this.options.redis,\n      server: !!this.options.server,\n      subprotocol: this.options.subprotocol\n    })\n  }\n\n  markAsProcessed(meta) {\n    this.log.changeMeta(meta.id, { status: 'processed' })\n    let data = parseId(meta.id)\n    if (data.userId !== 'server') {\n      this.log.add(\n        { id: meta.id, type: 'logux/processed' },\n        { clients: [data.clientId], status: 'processed' }\n      )\n    }\n  }\n\n  on(event, listener) {\n    if (event === 'preadd' || event === 'add' || event === 'clean') {\n      return this.log.emitter.on(event, listener)\n    } else {\n      return this.emitter.on(event, listener)\n    }\n  }\n\n  otherChannel(callbacks) {\n    normalizeChannelCallbacks('Unknown channel', callbacks)\n    if (this.otherSubscriber) {\n      throw new Error('Callbacks for unknown channel are already defined')\n    }\n    let channel = Object.assign({}, callbacks)\n    channel.pattern = {\n      match(name) {\n        return [name]\n      }\n    }\n    this.otherSubscriber = channel\n  }\n\n  otherType(callbacks) {\n    if (this.otherProcessor) {\n      throw new Error('Callbacks for unknown types are already defined')\n    }\n    normalizeTypeCallbacks('Unknown type', callbacks)\n    this.otherProcessor = callbacks\n  }\n\n  performUnsubscribe(clientNodeId, action, meta) {\n    if (action.channel === '__proto__' || clientNodeId === '__proto__') return\n    if (this.subscribers[action.channel]) {\n      let subscriber = this.subscribers[action.channel][clientNodeId]\n      if (subscriber) {\n        if (subscriber.unsubscribe) {\n          subscriber.unsubscribe(action, meta)\n          this.contexts.delete(action)\n        }\n        let filterId = subscriberFilterId(action)\n        delete subscriber.filters[filterId]\n        if (Object.keys(subscriber.filters).length === 0) {\n          delete this.subscribers[action.channel][clientNodeId]\n        }\n        if (Object.keys(this.subscribers[action.channel]).length === 0) {\n          delete this.subscribers[action.channel]\n        }\n      }\n    }\n    this.emitter.emit('unsubscribed', action, meta, clientNodeId)\n    this.emitter.emit('report', 'unsubscribed', {\n      actionId: meta.id,\n      channel: action.channel\n    })\n  }\n\n  process(action, meta = {}) {\n    return new Promise((resolve, reject) => {\n      let unbindError = this.on('error', (e, errorAction) => {\n        if (errorAction === action) {\n          unbindError()\n          unbindProcessed()\n          reject(e)\n        }\n      })\n      let unbindProcessed = this.on('processed', (processed, processedMeta) => {\n        if (processed === action) {\n          unbindError()\n          unbindProcessed()\n          resolve(processedMeta)\n        }\n      })\n      this.log.add(action, meta)\n    })\n  }\n\n  async processAction(processor, action, meta, start) {\n    let ctx = this.createContext(action, meta)\n\n    let latency\n    this.processing += 1\n    try {\n      await processor.process(ctx, action, meta)\n      latency = Date.now() - start\n      this.markAsProcessed(meta)\n    } catch (e) {\n      this.log.changeMeta(meta.id, { status: 'error' })\n      this.undo(action, meta, 'error')\n      this.emitter.emit('error', e, action, meta)\n    } finally {\n      this.finally(processor, ctx, action, meta)\n    }\n    if (typeof latency === 'undefined') latency = Date.now() - start\n    this.processing -= 1\n    this.emitter.emit('processed', action, meta, latency)\n  }\n\n  async processHttp(req, res) {\n    let urlString = req.url\n    if (/^\\/\\w+%3F/.test(urlString)) {\n      urlString = decodeURIComponent(urlString)\n    }\n    let reqUrl = new URL(urlString, 'http://localhost')\n    let rule = this.httpListeners[req.method + ' ' + reqUrl.pathname]\n\n    if (!rule) {\n      let processed = false\n      for (let listener of this.httpAllListeners) {\n        let result = await listener(req, res)\n        if (result === true) {\n          processed = true\n          break\n        }\n      }\n      if (!processed) {\n        res.writeHead(404, { 'Content-Type': 'text/plain' })\n        res.end('Not found\\n')\n      }\n    } else {\n      await rule(req, res)\n    }\n  }\n\n  rememberBadAuth(ip) {\n    this.authAttempts[ip] = (this.authAttempts[ip] || 0) + 1\n    this.setTimeout(() => {\n      if (this.authAttempts[ip] === 1) {\n        delete this.authAttempts[ip]\n      } else {\n        this.authAttempts[ip] -= 1\n      }\n    }, 3000)\n  }\n\n  replaceResendShortcuts(meta) {\n    if (meta.channel) {\n      meta.channels = [meta.channel]\n      delete meta.channel\n    }\n    if (meta.user) {\n      meta.users = [meta.user]\n      delete meta.user\n    }\n    if (meta.client) {\n      meta.clients = [meta.client]\n      delete meta.client\n    }\n    if (meta.node) {\n      meta.nodes = [meta.node]\n      delete meta.node\n    }\n  }\n\n  async sendAction(action, meta) {\n    let from = parseId(meta.id).clientId\n    let ignoreClients = new Set(meta.excludeClients || [])\n    ignoreClients.add(from)\n\n    if (meta.nodes) {\n      for (let id of meta.nodes) {\n        let client = this.nodeIds.get(id)\n        if (client) {\n          ignoreClients.add(client.clientId)\n          client.node.onAdd(action, meta)\n        }\n      }\n    }\n\n    if (meta.clients) {\n      for (let id of meta.clients) {\n        if (this.clientIds.has(id)) {\n          let client = this.clientIds.get(id)\n          ignoreClients.add(client.clientId)\n          client.node.onAdd(action, meta)\n        }\n      }\n    }\n\n    if (meta.users) {\n      for (let userId of meta.users) {\n        let users = this.userIds.get(userId)\n        if (users) {\n          for (let client of users) {\n            if (!ignoreClients.has(client.clientId)) {\n              ignoreClients.add(client.clientId)\n              client.node.onAdd(action, meta)\n            }\n          }\n        }\n      }\n    }\n\n    if (meta.channels) {\n      for (let channel of meta.channels) {\n        if (this.subscribers[channel]) {\n          for (let nodeId in this.subscribers[channel]) {\n            let clientId = parseId(nodeId).clientId\n            if (!ignoreClients.has(clientId)) {\n              let subscriber = this.subscribers[channel][nodeId]\n              if (subscriber) {\n                let ctx = this.createContext(action, meta)\n                let client = this.clientIds.get(clientId)\n                for (let filter of Object.values(subscriber.filters)) {\n                  filter =\n                    typeof filter === 'function'\n                      ? await filter(ctx, action, meta)\n                      : filter\n                  if (filter && client) {\n                    ignoreClients.add(clientId)\n                    client.node.onAdd(action, meta)\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  sendOnConnect(loader) {\n    this.connectLoader = loader\n  }\n\n  setTimeout(callback, ms) {\n    this.lastTimeout += 1\n    let id = this.lastTimeout\n    this.timeouts[id] = setTimeout(() => {\n      delete this.timeouts[id]\n      callback()\n    }, ms)\n  }\n\n  subscribe(nodeId, channel) {\n    if (channel === '__proto__' || nodeId === '__proto__') return\n    if (!this.subscribers[channel] || !this.subscribers[channel][nodeId]) {\n      if (!this.subscribers[channel]) {\n        this.subscribers[channel] = {}\n      }\n      this.subscribers[channel][nodeId] = { filters: { '{}': true } }\n      this.log.add({ channel, type: 'logux/subscribed' }, { nodes: [nodeId] })\n    }\n  }\n\n  async subscribeAction(action, meta, start) {\n    if (typeof action.channel !== 'string' || action.channel === '__proto__') {\n      this.wrongChannel(action, meta)\n      return\n    }\n\n    let channels = this.channels\n    if (this.otherSubscriber) {\n      channels = this.channels.concat([this.otherSubscriber])\n    }\n\n    let match\n    for (let channel of channels) {\n      if (channel.pattern) {\n        match = channel.pattern.match(action.channel)\n      } else {\n        match = action.channel.match(channel.regexp)\n      }\n\n      let subscribed = false\n      if (match) {\n        let ctx = this.createContext(action, meta)\n        if (ctx.nodeId === '__proto__') return\n        ctx.params = match\n        try {\n          let access = await channel.access(ctx, action, meta)\n          if (this.wrongChannels[meta.id]) {\n            delete this.wrongChannels[meta.id]\n            return\n          }\n          if (!access) {\n            this.denyAction(action, meta)\n            return\n          }\n\n          let client = this.clientIds.get(ctx.clientId)\n          if (!client) {\n            this.emitter.emit('subscriptionCancelled')\n            return\n          }\n\n          let filterId = subscriberFilterId(action)\n          let filters = { [filterId]: true }\n\n          if (channel.filter) {\n            let filter = await channel.filter(ctx, action, meta)\n            filters = { [filterId]: filter }\n          }\n\n          this.emitter.emit('report', 'subscribed', {\n            actionId: meta.id,\n            channel: action.channel\n          })\n\n          if (!this.subscribers[action.channel]) {\n            this.subscribers[action.channel] = {}\n            this.emitter.emit('subscribing', action, meta)\n          }\n          let subscriber = this.subscribers[action.channel][ctx.nodeId]\n          if (subscriber) {\n            filters = { ...subscriber.filters, ...filters }\n          }\n          this.subscribers[action.channel][ctx.nodeId] = {\n            filters,\n            unsubscribe: channel.unsubscribe\n              ? (unsubscribeAction, unsubscribeMeta) =>\n                  channel.unsubscribe(ctx, unsubscribeAction, unsubscribeMeta)\n              : undefined\n          }\n          subscribed = true\n\n          if (channel.load) {\n            let sendBack = await channel.load(ctx, action, meta)\n            if (Array.isArray(sendBack)) {\n              await Promise.all(\n                sendBack.map(i => {\n                  return Array.isArray(i) ? ctx.sendBack(...i) : ctx.sendBack(i)\n                })\n              )\n            } else if (sendBack) {\n              await ctx.sendBack(sendBack)\n            }\n          }\n          this.emitter.emit('subscribed', action, meta, Date.now() - start)\n          this.markAsProcessed(meta)\n        } catch (e) {\n          if (e.name === 'LoguxNotFoundError') {\n            this.undo(action, meta, 'notFound')\n          } else {\n            this.emitter.emit('error', e, action, meta)\n            this.undo(action, meta, 'error')\n          }\n          if (subscribed) {\n            this.unsubscribe(action, meta)\n          }\n        } finally {\n          this.finally(channel, ctx, action, meta)\n        }\n        break\n      }\n    }\n\n    if (!match) this.wrongChannel(action, meta)\n  }\n\n  type(name, callbacks, options = {}) {\n    let queue = options.queue || 'main'\n    this.typeToQueue.set(name, queue)\n\n    if (typeof name === 'function') name = name.type\n    normalizeTypeCallbacks(`Action type ${name}`, callbacks)\n\n    if (name instanceof RegExp) {\n      this.regexTypes.set(name, callbacks)\n    } else {\n      if (this.types[name]) {\n        throw new Error(`Action type ${name} was already defined`)\n      }\n      this.types[name] = callbacks\n    }\n  }\n\n  undo(action, meta, reason = 'error', extra = {}) {\n    let clientId = parseId(meta.id).clientId\n    let [undoAction, undoMeta] = this.buildUndo(action, meta, reason, extra)\n    undoMeta.clients = (undoMeta.clients || []).concat([clientId])\n    return this.log.add(undoAction, undoMeta)\n  }\n\n  unknownType(action, meta) {\n    this.internalUnknownType(action, meta)\n    this.unknownTypes[meta.id] = true\n  }\n\n  unsubscribe(action, meta) {\n    let clientNodeId = meta.id.split(' ')[1]\n    this.performUnsubscribe(clientNodeId, action, meta)\n  }\n\n  unsubscribeAction(action, meta) {\n    if (typeof action.channel !== 'string') {\n      this.wrongChannel(action, meta)\n      return\n    }\n\n    this.unsubscribe(action, meta)\n\n    this.markAsProcessed(meta)\n    this.contexts.delete(action)\n  }\n\n  wrongChannel(action, meta) {\n    this.internalWrongChannel(action, meta)\n    this.wrongChannels[meta.id] = true\n  }\n}\n"
  },
  {
    "path": "base-server/index.test.ts",
    "content": "import { defineAction } from '@logux/actions'\nimport {\n  type Action,\n  Log,\n  MemoryStore,\n  type TestLog,\n  TestTime\n} from '@logux/core'\nimport { restoreAll, spy, type Spy, spyOn } from 'nanospy'\nimport { readFileSync } from 'node:fs'\nimport http from 'node:http'\nimport https from 'node:https'\nimport { join } from 'node:path'\nimport { setTimeout } from 'node:timers/promises'\nimport { afterEach, expect, it } from 'vitest'\nimport WebSocket from 'ws'\n\nimport {\n  BaseServer,\n  type BaseServerOptions,\n  type ServerMeta\n} from '../index.js'\n\nconst ROOT = join(import.meta.dirname, '..')\n\nconst DEFAULT_OPTIONS = {\n  minSubprotocol: 0,\n  subprotocol: 0\n}\nconst CERT = join(ROOT, 'test/fixtures/cert.pem')\nconst KEY = join(ROOT, 'test/fixtures/key.pem')\n\nlet lastPort = 9111\n\nfunction createServer(\n  options: Partial<BaseServerOptions> = {}\n): BaseServer<object, TestLog<ServerMeta>> {\n  let opts = {\n    ...DEFAULT_OPTIONS,\n    ...options\n  }\n  if (typeof opts.time === 'undefined') {\n    opts.time = new TestTime()\n    opts.id = 'uuid'\n  }\n  if (typeof opts.port === 'undefined') {\n    lastPort += 1\n    opts.port = lastPort\n  }\n\n  let created = new BaseServer<object, TestLog<ServerMeta>>(opts)\n  created.auth(() => true)\n\n  destroyable = created\n\n  return created\n}\n\nlet destroyable: BaseServer | undefined\nlet httpServer: http.Server | undefined\n\nfunction createReporter(opts: Partial<BaseServerOptions> = {}): {\n  app: BaseServer<object, TestLog<ServerMeta>>\n  names: string[]\n  reports: [string, any][]\n} {\n  let names: string[] = []\n  let reports: [string, any][] = []\n\n  let app = createServer(opts)\n  app.on('report', (name: string, details?: any) => {\n    names.push(name)\n    if (details?.meta) {\n      details.meta = JSON.parse(JSON.stringify(details.meta))\n    }\n    reports.push([name, details])\n  })\n  return { app, names, reports }\n}\n\nlet originEnv = process.env.NODE_ENV\n\nfunction privateMethods(obj: object): any {\n  return obj\n}\n\nfunction emit(obj: any, event: string, ...args: any): void {\n  obj.emitter.emit(event, ...args)\n}\n\nasync function catchError(cb: () => Promise<any>): Promise<Error> {\n  try {\n    await cb()\n  } catch (e) {\n    if (e instanceof Error) return e\n  }\n  throw new Error('Error was not thrown')\n}\n\nfunction calls(fn: Function | undefined): any[][] {\n  return (fn as any as Spy).calls\n}\n\nfunction called(fn: Function | undefined): boolean {\n  return (fn as any as Spy).called\n}\n\nfunction callCount(fn: Function | undefined): number {\n  return (fn as any as Spy).callCount\n}\n\nafterEach(async () => {\n  restoreAll()\n  process.env.NODE_ENV = originEnv\n  if (destroyable) {\n    await destroyable.destroy()\n    destroyable = undefined\n  }\n  if (httpServer) httpServer.close()\n})\n\nit('saves server options', () => {\n  let app = new BaseServer({\n    minSubprotocol: 1,\n    subprotocol: 1\n  })\n  expect(app.options.minSubprotocol).toEqual(1)\n})\n\nit('generates node ID', () => {\n  let app = new BaseServer({\n    minSubprotocol: 1,\n    subprotocol: 1\n  })\n  expect(app.nodeId).toMatch(/^server:[\\w-]+$/)\n})\n\nit('throws on missed subprotocol', () => {\n  expect(() => {\n    new BaseServer({})\n  }).toThrow(/Missed `subprotocol` option/)\n})\n\nit('throws on missed supported subprotocols', () => {\n  expect(() => {\n    new BaseServer({ subprotocol: 0 })\n  }).toThrow(/Missed `minSubprotocol` option/)\n})\n\nit('sets development environment by default', () => {\n  delete process.env.NODE_ENV\n  let app = new BaseServer(DEFAULT_OPTIONS)\n  expect(app.env).toEqual('development')\n})\n\nit('takes environment from NODE_ENV', () => {\n  process.env.NODE_ENV = 'production'\n  let app = new BaseServer(DEFAULT_OPTIONS)\n  expect(app.env).toEqual('production')\n})\n\nit('sets environment from user', () => {\n  let app = new BaseServer({\n    env: 'production',\n    minSubprotocol: 0,\n    subprotocol: 0\n  })\n  expect(app.env).toEqual('production')\n})\n\nit('uses cwd as default root', () => {\n  let app = new BaseServer(DEFAULT_OPTIONS)\n  expect(app.options.root).toEqual(process.cwd())\n})\n\nit('supports string as port', () => {\n  let app = new BaseServer({ ...DEFAULT_OPTIONS, port: '8080' })\n  expect(app.options.port).toEqual(8080)\n})\n\nit('uses user root', () => {\n  let app = new BaseServer({\n    minSubprotocol: 0,\n    root: '/a',\n    subprotocol: 0\n  })\n  expect(app.options.root).toEqual('/a')\n})\n\nit('creates log with default store', () => {\n  let app = new BaseServer(DEFAULT_OPTIONS)\n  expect(app.log instanceof Log).toBe(true)\n  expect(app.log.store instanceof MemoryStore).toBe(true)\n})\n\nit('creates log with custom store', () => {\n  let store = new MemoryStore()\n  let app = new BaseServer({\n    minSubprotocol: 0,\n    store,\n    subprotocol: 0\n  })\n  expect(app.log.store).toBe(store)\n})\n\nit('uses test time and ID', () => {\n  let store = new MemoryStore()\n  let app = new BaseServer({\n    id: 'uuid',\n    minSubprotocol: 0,\n    store,\n    subprotocol: 0,\n    time: new TestTime()\n  })\n  expect(app.log.store).toEqual(store)\n  expect(app.log.generateId()).toEqual('1 server:uuid 0')\n})\n\nit('destroys application without runned server', async () => {\n  let app = new BaseServer(DEFAULT_OPTIONS)\n  await app.destroy()\n  app.destroy()\n})\n\nit('throws without authenticator', async () => {\n  expect.assertions(1)\n  let app = new BaseServer(DEFAULT_OPTIONS)\n  let error = await catchError(() => app.listen())\n  expect(error.message).toMatch(/authentication/)\n})\n\nit('sets default ports and hosts', () => {\n  let app = createServer()\n  expect(app.options.port).toEqual(31337)\n  expect(app.options.host).toEqual('127.0.0.1')\n})\n\nit('uses user port', () => {\n  let app = createServer({ port: 1337 })\n  expect(app.options.port).toEqual(1337)\n})\n\nit('throws a error on key without certificate', () => {\n  expect(() => {\n    createServer({ key: readFileSync(KEY).toString() })\n  }).toThrow(/set `cert` option/)\n})\n\nit('throws a error on certificate without key', () => {\n  expect(() => {\n    createServer({ cert: readFileSync(CERT).toString() })\n  }).toThrow(/set `key` option/)\n})\n\nit('uses HTTPS', async () => {\n  let app = createServer({\n    cert: readFileSync(CERT).toString(),\n    key: readFileSync(KEY).toString()\n  })\n  await app.listen()\n  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)\n})\n\nit('loads keys by absolute path', async () => {\n  let app = createServer({\n    cert: CERT,\n    key: KEY\n  })\n  await app.listen()\n  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)\n})\n\nit('loads keys by relative path', async () => {\n  let app = createServer({\n    cert: 'fixtures/cert.pem',\n    key: 'fixtures/key.pem',\n    root: join(ROOT, 'test/')\n  })\n  await app.listen()\n  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)\n})\n\nit('supports object in SSL key', async () => {\n  let app = createServer({\n    cert: readFileSync(CERT).toString(),\n    key: { pem: readFileSync(KEY).toString() }\n  })\n  await app.listen()\n  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)\n})\n\nit('reporters on start listening', async () => {\n  let test = createReporter({\n    redis: '//localhost'\n  })\n  let pkgFile = readFileSync(join(ROOT, 'package.json'))\n  let pkg = JSON.parse(pkgFile.toString())\n\n  privateMethods(test.app).listenNotes.prometheus =\n    'http://127.0.0.1:31338/prometheus'\n\n  let promise = test.app.listen()\n  expect(test.reports).toEqual([])\n\n  await promise\n  expect(test.reports).toEqual([\n    [\n      'listen',\n      {\n        cert: false,\n        environment: 'test',\n        host: '127.0.0.1',\n        loguxServer: pkg.version,\n        minSubprotocol: 0,\n        nodeId: 'server:uuid',\n        notes: {\n          prometheus: 'http://127.0.0.1:31338/prometheus'\n        },\n        port: test.app.options.port,\n        redis: '//localhost',\n        server: false,\n        subprotocol: 0\n      }\n    ]\n  ])\n})\n\nit('reporters on log events', async () => {\n  let test = createReporter()\n  test.app.type('A', { access: () => true })\n  test.app.type('B', { access: () => true })\n  await test.app.log.add({ type: 'A' }, { reasons: ['some'] })\n  await test.app.log.add({ type: 'B' })\n  await test.app.log.removeReason('some')\n  expect(test.reports).toEqual([\n    [\n      'add',\n      {\n        action: {\n          type: 'A'\n        },\n        meta: {\n          added: 1,\n          id: '1 server:uuid 0',\n          reasons: ['some'],\n          server: 'server:uuid',\n          status: 'waiting',\n          subprotocol: 0,\n          time: 1\n        }\n      }\n    ],\n    [\n      'addClean',\n      {\n        action: {\n          type: 'B'\n        },\n        meta: {\n          id: '2 server:uuid 0',\n          reasons: [],\n          server: 'server:uuid',\n          status: 'waiting',\n          subprotocol: 0,\n          time: 2\n        }\n      }\n    ],\n    [\n      'clean',\n      {\n        actionId: '1 server:uuid 0'\n      }\n    ]\n  ])\n})\n\nit('reporters on destroying', () => {\n  let test = createReporter()\n  let promise = test.app.destroy()\n  expect(test.reports).toEqual([['destroy', undefined]])\n  return promise\n})\n\nit('creates a client on connection', async () => {\n  let app = createServer()\n  await app.listen()\n  let ws = new WebSocket(`ws://127.0.0.1:${app.options.port}`)\n  await new Promise((resolve, reject) => {\n    ws.onopen = resolve\n    ws.onerror = reject\n  })\n  expect(app.connected.size).toEqual(1)\n  expect(app.connected.get('1')?.remoteAddress).toEqual('127.0.0.1')\n})\n\nit('creates a client manually', () => {\n  let app = createServer()\n  app.addClient({\n    on: () => {\n      return () => true\n    },\n    ws: {\n      _socket: {\n        remoteAddress: '127.0.0.1'\n      },\n      upgradeReq: {\n        headers: {}\n      }\n    }\n  } as any)\n  expect(app.connected.size).toEqual(1)\n  expect(app.connected.get('1')?.remoteAddress).toEqual('127.0.0.1')\n})\n\nit('sends debug message to clients on runtimeError', () => {\n  let app = createServer()\n  app.connected.set('1', {\n    connection: {\n      connected: true,\n      send: spy()\n    },\n    destroy: () => false\n  } as any)\n  app.connected.set('2', {\n    connection: {\n      connected: false,\n      send: spy()\n    },\n    destroy: () => false\n  } as any)\n  app.connected.set('3', {\n    connection: {\n      connected: true,\n      send: () => {\n        throw new Error()\n      }\n    },\n    destroy: () => false\n  } as any)\n\n  let error = new Error('Test Error')\n  error.stack = `${error.stack?.split('\\n')[0]}\\nfake stacktrace`\n\n  app.debugError(error)\n  expect(calls(app.connected.get('1')?.connection.send)).toEqual([\n    [['debug', 'error', 'Error: Test Error\\nfake stacktrace']]\n  ])\n  expect(called(app.connected.get('2')?.connection.send)).toBe(false)\n})\n\nit('disconnects client on destroy', () => {\n  let app = createServer()\n  app.connected.set('1', { destroy: spy() } as any)\n  app.destroy()\n  expect(callCount(app.connected.get('1')?.destroy)).toEqual(1)\n})\n\nit('accepts custom HTTP server', async () => {\n  httpServer = http.createServer()\n  let app = createServer({ server: httpServer })\n\n  await new Promise<void>(resolve => {\n    httpServer?.listen(app.options.port, resolve)\n  })\n  await app.listen()\n\n  let ws = new WebSocket(`ws://localhost:${app.options.port}`)\n  await new Promise((resolve, reject) => {\n    ws.onopen = resolve\n    ws.onerror = reject\n  })\n  expect(app.connected.size).toEqual(1)\n})\n\nit('marks actions with own node ID', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  let servers: string[] = []\n  app.on('add', (action, meta) => {\n    servers.push(meta.server)\n  })\n\n  await app.log.add({ type: 'A' })\n  await app.log.add({ type: 'A' }, { server: 'server2' })\n  expect(servers).toEqual([app.nodeId, 'server2'])\n})\n\nit('marks actions with waiting status', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n  app.channel('a', { access: () => true })\n\n  let statuses: (string | undefined)[] = []\n  app.on('add', (action, meta) => {\n    statuses.push(meta.status)\n  })\n\n  await app.log.add({ type: 'A' })\n  await app.log.add({ type: 'A' }, { status: 'processed' })\n  await app.log.add({ channel: 'a', type: 'logux/subscribe' })\n  expect(statuses).toEqual(['waiting', 'processed', undefined])\n})\n\nit('defines actions types', () => {\n  let app = createServer()\n  app.type('FOO', { access: () => true })\n  expect(privateMethods(app).types.FOO).not.toBeUndefined()\n})\n\nit('does not allow to define type twice', () => {\n  let app = createServer()\n  app.type('FOO', { access: () => true })\n  expect(() => {\n    app.type('FOO', { access: () => true })\n  }).toThrow(/already/)\n})\n\nit('requires access callback for type', () => {\n  let app = createServer()\n  expect(() => {\n    // @ts-expect-error\n    app.type('FOO')\n  }).toThrow(/access callback/)\n})\n\nit('reports about unknown action type', async () => {\n  let test = createReporter()\n  await test.app.log.add({ type: 'UNKNOWN' }, { id: '1 10:uuid 0' })\n  expect(test.names).toEqual(['addClean', 'unknownType', 'addClean'])\n  expect(test.reports[1]).toEqual([\n    'unknownType',\n    {\n      actionId: '1 10:uuid 0',\n      type: 'UNKNOWN'\n    }\n  ])\n})\n\nit('ignores unknown type for processed actions', async () => {\n  let test = createReporter()\n  await test.app.log.add(\n    { type: 'A' },\n    { channels: ['a'], status: 'processed' }\n  )\n  expect(test.names).toEqual(['addClean'])\n})\n\nit('reports about fatal error', () => {\n  let test = createReporter({ env: 'development' })\n\n  let err = new Error('Test')\n  emit(test.app, 'fatal', err)\n\n  expect(test.reports).toEqual([['error', { err, fatal: true }]])\n})\n\nit('sends errors to clients in development', () => {\n  let test = createReporter({ env: 'development' })\n  test.app.connected.set('0', {\n    connection: { connected: true, send: spy() },\n    destroy: () => false\n  } as any)\n\n  let err = new Error('Test')\n  err.stack = 'stack'\n  privateMethods(err).nodeId = '10:uuid'\n  emit(test.app, 'error', err)\n\n  expect(test.reports).toEqual([['error', { err, nodeId: '10:uuid' }]])\n  expect(calls(test.app.connected.get('0')?.connection.send)).toEqual([\n    [['debug', 'error', 'stack']]\n  ])\n})\n\nit('does not send errors in non-development mode', () => {\n  let app = createServer({ env: 'production' })\n  app.connected.set('0', {\n    connection: { send: spy() },\n    destroy: () => false\n  } as any)\n  emit(app, 'error', new Error('Test'))\n  expect(called(app.connected.get('0')?.connection.send)).toBe(false)\n})\n\nit('processes actions', async () => {\n  let test = createReporter()\n  let processed: Action[] = []\n  let fired: Action[] = []\n\n  test.app.type('FOO', {\n    access: () => true,\n    async process(ctx, action, meta) {\n      expect(meta.added).toEqual(1)\n      expect(ctx.isServer).toBe(true)\n      await setTimeout(25)\n      processed.push(action)\n    }\n  })\n  test.app.on('processed', (action, meta, latency) => {\n    expect(typeof latency).toEqual('number')\n    expect(meta.added).toEqual(1)\n    fired.push(action)\n  })\n\n  await test.app.log.add({ type: 'FOO' }, { reasons: ['test'] })\n  expect(fired).toEqual([])\n  expect(test.app.log.entries()[0][1].status).toEqual('waiting')\n  await setTimeout(30)\n  expect(test.app.log.entries()[0][1].status).toEqual('processed')\n  expect(processed).toEqual([{ type: 'FOO' }])\n  expect(fired).toEqual([{ type: 'FOO' }])\n})\n\nit('processes regex matching action', async () => {\n  let test = createReporter()\n  let processed: Action[] = []\n  let fired: Action[] = []\n\n  test.app.type(/.*TODO$/, {\n    access: () => true,\n    async process(ctx, action, meta) {\n      expect(meta.added).toEqual(1)\n      expect(ctx.isServer).toBe(true)\n      await setTimeout(25)\n      processed.push(action)\n    }\n  })\n  test.app.on('processed', (action, meta, latency) => {\n    expect(typeof latency).toEqual('number')\n    expect(meta.added).toEqual(1)\n    fired.push(action)\n  })\n\n  await test.app.log.add({ type: 'ADD_TODO' }, { reasons: ['test'] })\n  expect(fired).toEqual([])\n  expect(test.app.log.entries()[0][1].status).toEqual('waiting')\n  await setTimeout(30)\n  expect(test.app.log.entries()[0][1].status).toEqual('processed')\n  expect(processed).toEqual([{ type: 'ADD_TODO' }])\n  expect(fired).toEqual([{ type: 'ADD_TODO' }])\n})\n\nit('has full events API', () => {\n  let app = createServer()\n\n  let events = 0\n  let unbind = app.on('processed', () => {\n    events += 1\n  })\n\n  emit(app, 'processed', { type: 'FOO' }, { id: '1 1:1 0' })\n  emit(app, 'processed', { type: 'FOO' }, { id: '1 1:1 0' })\n  unbind()\n  emit(app, 'processed', { type: 'FOO' }, { id: '1 1:1 0' })\n\n  expect(events).toEqual(2)\n})\n\nit('waits for last processing before destroy', async () => {\n  let app = createServer()\n\n  let started = 0\n  let process: (() => void) | undefined\n\n  app.type('FOO', {\n    access: () => true,\n    process() {\n      started += 1\n      return new Promise(resolve => {\n        process = resolve\n      })\n    }\n  })\n\n  let destroyed = false\n  await app.log.add({ type: 'FOO' })\n  app.destroy().then(() => {\n    destroyed = true\n  })\n  await setTimeout(1)\n\n  expect(destroyed).toBe(false)\n  expect(privateMethods(app).processing).toEqual(1)\n  await app.log.add({ type: 'FOO' })\n\n  expect(started).toEqual(1)\n  if (typeof process === 'undefined') throw new Error('process is not set')\n  process()\n  await setTimeout(1)\n\n  expect(destroyed).toBe(true)\n})\n\nit('reports about error during action processing', async () => {\n  let test = createReporter()\n\n  let err = new Error('Test')\n  test.app.type('FOO', {\n    access: () => true,\n    process() {\n      throw err\n    }\n  })\n\n  await test.app.log.add({ type: 'FOO' }, { reasons: ['test'] })\n  await setTimeout(1)\n\n  expect(test.names).toEqual(['add', 'error', 'add'])\n  expect(test.reports[1]).toEqual([\n    'error',\n    {\n      actionId: '1 server:uuid 0',\n      err\n    }\n  ])\n  expect(test.reports[2][1].action).toEqual({\n    action: { type: 'FOO' },\n    id: '1 server:uuid 0',\n    reason: 'error',\n    type: 'logux/undo'\n  })\n})\n\nit('undoes actions on client', async () => {\n  let app = createServer()\n  app.undo(\n    { type: 'FOO' },\n    {\n      added: 1,\n      channels: ['user/1'],\n      clients: ['2:client'],\n      excludeClients: ['3:client'],\n      id: '1 1:client:uuid 0',\n      nodes: ['2:client:uuid'],\n      reasons: ['user/1/lastValue'],\n      server: 'server:uuid',\n      time: 1,\n      users: ['3']\n    },\n    'magic',\n    {\n      one: 1\n    }\n  )\n\n  expect(app.log.entries()).toEqual([\n    [\n      {\n        action: {\n          type: 'FOO'\n        },\n        id: '1 1:client:uuid 0',\n        one: 1,\n        reason: 'magic',\n        type: 'logux/undo'\n      },\n      {\n        added: 1,\n        channels: ['user/1'],\n        clients: ['2:client', '1:client'],\n        excludeClients: ['3:client'],\n        id: '1 server:uuid 0',\n        nodes: ['2:client:uuid'],\n        reasons: ['user/1/lastValue'],\n        server: 'server:uuid',\n        status: 'processed',\n        subprotocol: 0,\n        time: 1,\n        users: ['3']\n      }\n    ]\n  ])\n})\n\nit('adds current subprotocol to meta', async () => {\n  let app = createServer({ subprotocol: 1 })\n  app.type('A', { access: () => true })\n  await app.log.add({ type: 'A' }, { reasons: ['test'] })\n  expect(app.log.entries()[0][1].subprotocol).toEqual(1)\n})\n\nit('adds current subprotocol only to own actions', async () => {\n  let app = createServer({ subprotocol: 1 })\n  app.type('A', { access: () => true })\n  await app.log.add({ type: 'A' }, { id: '1 0:other 0', reasons: ['test'] })\n  expect(app.log.entries()[0][1].subprotocol).toBeUndefined()\n})\n\nit('allows to override subprotocol in meta', async () => {\n  let app = createServer({ subprotocol: 2 })\n  app.type('A', { access: () => true })\n  await app.log.add({ type: 'A' }, { reasons: ['test'], subprotocol: 1 })\n  expect(app.log.entries()[0][1].subprotocol).toEqual(1)\n})\n\nit('checks channel definition', () => {\n  let app = createServer()\n\n  expect(() => {\n    // @ts-expect-error\n    app.channel('foo/:id')\n  }).toThrow('Channel foo/:id must have access callback')\n\n  expect(() => {\n    // @ts-expect-error\n    app.channel(/^foo:/, { load: true })\n  }).toThrow('Channel /^foo:/ must have access callback')\n})\n\nit('reports about wrong channel name', async () => {\n  let test = createReporter({ env: 'development' })\n  test.app.channel('foo', { access: () => true })\n  let client: any = {\n    connection: { send: spy() },\n    node: { onAdd() {} }\n  }\n  test.app.nodeIds.set('10:uuid', client)\n  test.app.clientIds.set('10:uuid', client)\n  await test.app.log.add({ type: 'logux/subscribe' }, { id: '1 10:uuid 0' })\n  expect(test.names).toEqual(['addClean', 'wrongChannel', 'addClean'])\n  expect(test.reports[1][1]).toEqual({\n    actionId: '1 10:uuid 0',\n    channel: undefined\n  })\n  expect(test.reports[2][1].action).toEqual({\n    action: { type: 'logux/subscribe' },\n    id: '1 10:uuid 0',\n    reason: 'wrongChannel',\n    type: 'logux/undo'\n  })\n  expect(calls(client.connection.send)).toEqual([\n    [['debug', 'error', 'Wrong channel name undefined']]\n  ])\n  await test.app.log.add({ type: 'logux/unsubscribe' })\n\n  expect(test.reports[4]).toEqual([\n    'wrongChannel',\n    {\n      actionId: '2 server:uuid 0',\n      channel: undefined\n    }\n  ])\n  await test.app.log.add({ channel: 'unknown', type: 'logux/subscribe' })\n\n  expect(test.reports[7]).toEqual([\n    'wrongChannel',\n    {\n      actionId: '4 server:uuid 0',\n      channel: 'unknown'\n    }\n  ])\n})\n\nit('checks custom channel name subscriber', () => {\n  let app = createServer()\n\n  expect(() => {\n    // @ts-expect-error\n    app.otherChannel()\n  }).toThrow('Unknown channel must have access callback')\n\n  app.otherChannel({ access: () => true })\n  expect(() => {\n    app.otherChannel({ access: () => true })\n  }).toThrow('Callbacks for unknown channel are already defined')\n})\n\nit('allows to have custom channel name check', async () => {\n  let test = createReporter()\n  let channels: string[] = []\n  test.app.otherChannel({\n    access(ctx, action, meta) {\n      channels.push(ctx.params[0])\n      test.app.wrongChannel(action, meta)\n      return false\n    }\n  })\n  let client: any = {\n    connection: { send() {} },\n    node: { onAdd() {} }\n  }\n  test.app.nodeIds.set('10:uuid', client)\n  test.app.clientIds.set('10:uuid', client)\n  await test.app.log.add({ channel: 'foo', type: 'logux/subscribe' })\n  expect(channels).toEqual(['foo'])\n  expect(test.names).toEqual(['addClean', 'wrongChannel', 'addClean'])\n})\n\nit('ignores subscription for other servers', async () => {\n  let test = createReporter()\n  let action = { type: 'logux/subscribe' }\n  await test.app.log.add(action, { server: 'server:other' })\n  expect(test.names).toEqual(['addClean'])\n})\n\nit('checks channel access', async () => {\n  let test = createReporter()\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  test.app.nodeIds.set('10:uuid', client)\n  test.app.clientIds.set('10:uuid', client)\n\n  let finalled = 0\n\n  test.app.channel(/^user\\/(\\d+)$/, {\n    async access(ctx) {\n      expect(ctx.params[1]).toEqual('10')\n      return false\n    },\n    finally() {\n      finalled += 1\n    }\n  })\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: '1 10:uuid 0' }\n  )\n  await setTimeout(1)\n\n  expect(test.names).toEqual(['addClean', 'denied', 'addClean'])\n  expect(test.reports[1][1]).toEqual({ actionId: '1 10:uuid 0' })\n  expect(test.reports[2][1].action).toEqual({\n    action: { channel: 'user/10', type: 'logux/subscribe' },\n    id: '1 10:uuid 0',\n    reason: 'denied',\n    type: 'logux/undo'\n  })\n  expect(test.app.subscribers).toEqual({})\n  expect(finalled).toEqual(1)\n})\n\nit('reports about errors during channel authorization', async () => {\n  let test = createReporter()\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  test.app.nodeIds.set('10:uuid', client)\n  test.app.clientIds.set('10:uuid', client)\n\n  let err = new Error()\n  test.app.channel(/^user\\/(\\d+)$/, {\n    access() {\n      throw err\n    }\n  })\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: '1 10:uuid 0' }\n  )\n  await Promise.resolve()\n  await Promise.resolve()\n\n  expect(test.names).toEqual(['addClean', 'error', 'addClean'])\n  expect(test.reports[1][1]).toEqual({ actionId: '1 10:uuid 0', err })\n  expect(test.reports[2][1].action).toEqual({\n    action: { channel: 'user/10', type: 'logux/subscribe' },\n    id: '1 10:uuid 0',\n    reason: 'error',\n    type: 'logux/undo'\n  })\n  expect(test.app.subscribers).toEqual({})\n})\n\nit('subscribes clients', async () => {\n  let test = createReporter()\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  test.app.nodeIds.set('10:a:uuid', client)\n  test.app.clientIds.set('10:a', client)\n\n  let userSubsriptions = 0\n  test.app.channel<{ id: string }>('user/:id', {\n    access(ctx, action, meta) {\n      expect(ctx.params.id).toEqual('10')\n      expect(action.channel).toEqual('user/10')\n      expect(meta.id).toEqual('1 10:a:uuid 0')\n      expect(ctx.nodeId).toEqual('10:a:uuid')\n      userSubsriptions += 1\n      return true\n    }\n  })\n\n  let filter = (): boolean => false\n  test.app.channel('posts', {\n    access() {\n      return true\n    },\n    async filter() {\n      return filter\n    }\n  })\n\n  let events = 0\n  test.app.on('subscribed', (action, meta, latency) => {\n    expect(action.type).toEqual('logux/subscribe')\n    expect(meta.id).toContain('10:a:uuid')\n    expect(latency).toBeCloseTo(25, -2)\n    events += 1\n  })\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: '1 10:a:uuid 0' }\n  )\n  await setTimeout(1)\n  expect(events).toEqual(1)\n  expect(userSubsriptions).toEqual(1)\n  expect(test.names).toEqual(['addClean', 'subscribed', 'addClean'])\n  expect(test.reports[1][1]).toEqual({\n    actionId: '1 10:a:uuid 0',\n    channel: 'user/10'\n  })\n  expect(test.reports[2][1].action).toEqual({\n    id: '1 10:a:uuid 0',\n    type: 'logux/processed'\n  })\n  expect(test.reports[2][1].meta.clients).toEqual(['10:a'])\n  expect(test.reports[2][1].meta.status).toEqual('processed')\n  expect(test.app.subscribers).toEqual({\n    'user/10': {\n      '10:a:uuid': { filters: { '{}': true } }\n    }\n  })\n  await test.app.log.add(\n    { channel: 'posts', type: 'logux/subscribe' },\n    { id: '2 10:a:uuid 0' }\n  )\n  await setTimeout(1)\n\n  expect(events).toEqual(2)\n  expect(test.app.subscribers).toEqual({\n    'posts': {\n      '10:a:uuid': { filters: { '{}': filter } }\n    },\n    'user/10': {\n      '10:a:uuid': { filters: { '{}': true } }\n    }\n  })\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/unsubscribe' },\n    { id: '3 10:a:uuid 0' }\n  )\n\n  expect(test.names).toEqual([\n    'addClean',\n    'subscribed',\n    'addClean',\n    'addClean',\n    'subscribed',\n    'addClean',\n    'addClean',\n    'unsubscribed',\n    'addClean'\n  ])\n  expect(test.reports[7][1]).toEqual({\n    actionId: '3 10:a:uuid 0',\n    channel: 'user/10'\n  })\n  expect(test.reports[8][1].action).toEqual({\n    id: '3 10:a:uuid 0',\n    type: 'logux/processed'\n  })\n  expect(test.app.subscribers).toEqual({\n    posts: {\n      '10:a:uuid': { filters: { '{}': filter } }\n    }\n  })\n})\n\nit('subscribes clients with multiple filters', async () => {\n  let test = createReporter()\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  test.app.nodeIds.set('10:a:uuid', client)\n  test.app.clientIds.set('10:a', client)\n\n  let filter = (): boolean => false\n  test.app.channel('posts', {\n    access() {\n      return true\n    },\n    async filter() {\n      return filter\n    }\n  })\n\n  await test.app.log.add(\n    { channel: 'posts', type: 'logux/subscribe' },\n    { id: '1 10:a:uuid 0' }\n  )\n  await test.app.log.add(\n    { channel: 'posts', filter: { category: 'a' }, type: 'logux/subscribe' },\n    { id: '1 10:a:uuid 0' }\n  )\n  await test.app.log.add(\n    { channel: 'posts', filter: { category: 'b' }, type: 'logux/subscribe' },\n    { id: '1 10:a:uuid 0' }\n  )\n  await setTimeout(1)\n  expect(test.app.subscribers).toEqual({\n    posts: {\n      '10:a:uuid': {\n        filters: {\n          '{\"category\":\"a\"}': filter,\n          '{\"category\":\"b\"}': filter,\n          '{}': filter\n        }\n      }\n    }\n  })\n\n  await test.app.log.add(\n    { channel: 'posts', type: 'logux/unsubscribe' },\n    { id: '2 10:a:uuid 0' }\n  )\n  await test.app.log.add(\n    { channel: 'posts', filter: { category: 'b' }, type: 'logux/unsubscribe' },\n    { id: '2 10:a:uuid 0' }\n  )\n  await setTimeout(1)\n  expect(test.app.subscribers).toEqual({\n    posts: {\n      '10:a:uuid': {\n        filters: { '{\"category\":\"a\"}': filter }\n      }\n    }\n  })\n})\n\nit('cancels subscriptions on disconnect', async () => {\n  let app = createServer()\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  app.nodeIds.set('10:uuid', client)\n  app.clientIds.set('10:uuid', client)\n\n  let cancels = 0\n  app.on('subscriptionCancelled', () => {\n    cancels += 1\n  })\n\n  app.channel('test', {\n    access() {\n      app.clientIds.delete('10:uuid')\n      app.nodeIds.delete('10:uuid')\n      return true\n    },\n    filter() {\n      throw new Error('no calls')\n    },\n    load() {\n      throw new Error('no calls')\n    }\n  })\n\n  await app.log.add(\n    { channel: 'test', type: 'logux/subscribe' },\n    { id: '1 10:uuid 0' }\n  )\n  await setTimeout(10)\n\n  expect(cancels).toEqual(1)\n})\n\nit('reports about errors during channel initialization', async () => {\n  let test = createReporter()\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  test.app.nodeIds.set('10:uuid', client)\n  test.app.clientIds.set('10:uuid', client)\n\n  let err = new Error()\n  test.app.channel(/^user\\/(\\d+)$/, {\n    access: () => true,\n    load() {\n      throw err\n    }\n  })\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: '1 10:uuid 0' }\n  )\n  await setTimeout(1)\n\n  expect(test.names).toEqual([\n    'addClean',\n    'subscribed',\n    'error',\n    'addClean',\n    'unsubscribed'\n  ])\n  expect(test.reports[2][1]).toEqual({ actionId: '1 10:uuid 0', err })\n  expect(test.reports[3][1].action).toEqual({\n    action: { channel: 'user/10', type: 'logux/subscribe' },\n    id: '1 10:uuid 0',\n    reason: 'error',\n    type: 'logux/undo'\n  })\n  expect(test.app.subscribers).toEqual({})\n})\n\nit('loads initial actions during subscription', async () => {\n  let test = createReporter({ time: new TestTime() })\n  let client: any = {\n    node: { onAdd: () => false, remoteSubprotocol: 0 }\n  }\n  test.app.nodeIds.set('10:uuid', client)\n  test.app.clientIds.set('10:uuid', client)\n\n  test.app.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n\n  let userLoaded = 0\n  let initializating: (() => void) | undefined\n  test.app.channel<{ id: string }>('user/:id', {\n    access: () => true,\n    load(ctx, action, meta) {\n      expect(ctx.params.id).toEqual('10')\n      expect(action.channel).toEqual('user/10')\n      expect(meta.id).toEqual('1 10:uuid 0')\n      expect(ctx.nodeId).toEqual('10:uuid')\n      userLoaded += 1\n      return new Promise(resolve => {\n        initializating = resolve\n      })\n    }\n  })\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: '1 10:uuid 0' }\n  )\n  await setTimeout(1)\n  expect(userLoaded).toEqual(1)\n  expect(test.app.subscribers).toEqual({\n    'user/10': {\n      '10:uuid': { filters: { '{}': true } }\n    }\n  })\n  expect(test.app.log.actions()).toEqual([\n    { channel: 'user/10', type: 'logux/subscribe' }\n  ])\n  if (typeof initializating === 'undefined') {\n    throw new Error('callback is not set')\n  }\n  initializating()\n  await setTimeout(1)\n\n  expect(test.app.log.actions()).toEqual([\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: '1 10:uuid 0', type: 'logux/processed' }\n  ])\n})\n\nit('calls unsubscribe() channel callback with logux/unsubscribe', async () => {\n  let test = createReporter({})\n  let client: any = {\n    node: {\n      onAdd: () => false,\n      remoteHeaders: { preservedHeaders: true },\n      remoteSubprotocol: 0\n    }\n  }\n  let nodeId = '10:uuid'\n  let clientId = '10:uuid'\n  let userId = '10'\n  test.app.nodeIds.set(nodeId, client)\n  test.app.clientIds.set(clientId, client)\n  test.app.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n  let unsubscribeCallback = spy()\n  test.app.channel<{ id: string }, { preservedData?: boolean }>('user/:id', {\n    access(ctx) {\n      ctx.data.preservedData = true\n      return true\n    },\n    unsubscribe: unsubscribeCallback\n  })\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: `1 ${nodeId}` }\n  )\n  expect(Object.keys(test.app.subscribers)).toHaveLength(1)\n\n  await test.app.log.add(\n    { channel: 'user/10', type: 'logux/unsubscribe' },\n    { id: `2 ${nodeId}` }\n  )\n  expect(Object.keys(test.app.subscribers)).toHaveLength(0)\n\n  expect(test.app.log.actions()).toEqual([\n    { channel: 'user/10', type: 'logux/subscribe' },\n    { id: `1 ${nodeId}`, type: 'logux/processed' },\n    { channel: 'user/10', type: 'logux/unsubscribe' },\n    { id: `2 ${nodeId}`, type: 'logux/processed' }\n  ])\n  expect(unsubscribeCallback.calls).toEqual([\n    [\n      expect.objectContaining({\n        clientId,\n        data: { preservedData: true },\n        headers: { preservedHeaders: true },\n        nodeId,\n        params: { id: '10' },\n        subprotocol: 0,\n        userId\n      }),\n      expect.objectContaining({\n        channel: 'user/10',\n        type: 'logux/unsubscribe'\n      }),\n      expect.objectContaining({\n        status: 'processed'\n      })\n    ]\n  ])\n})\n\nit('does not need type definition for own actions', async () => {\n  let test = createReporter()\n  await test.app.log.add({ type: 'unknown' }, { users: ['10'] })\n  expect(test.names).toEqual(['addClean'])\n  expect(test.reports[0][1].action.type).toEqual('unknown')\n  expect(test.reports[0][1].meta.status).toEqual('processed')\n})\n\nit('checks callbacks in unknown type handler', () => {\n  let app = createServer()\n\n  expect(() => {\n    // @ts-expect-error\n    app.otherType({ process: () => {} })\n  }).toThrow('Unknown type must have access callback')\n\n  app.otherType({ access: () => true })\n  expect(() => {\n    app.otherType({ access: () => true })\n  }).toThrow('Callbacks for unknown types are already defined')\n})\n\nit('reports about useless actions', async () => {\n  let test = createReporter()\n  test.app.type('known', {\n    access: () => true,\n    process: () => {}\n  })\n  test.app.channel('a', { access: () => true })\n  test.app.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n  await test.app.log.add({ type: 'unknown' }, { status: 'processed' })\n  await test.app.log.add({ type: 'known' })\n  await test.app.log.add({ channel: 'a', type: 'logux/subscribe' })\n  await test.app.log.add({ type: 'known' }, { channels: ['a'] })\n  await test.app.log.add({ type: 'known' }, { users: ['10'] })\n  await test.app.log.add({ type: 'known' }, { clients: ['10:client'] })\n  await test.app.log.add({ type: 'known' }, { nodes: ['10:client:uuid'] })\n  expect(test.names).toEqual([\n    'add',\n    'useless',\n    'add',\n    'add',\n    'add',\n    'add',\n    'add',\n    'add'\n  ])\n})\n\nit('has shortcuts for resend arrays', async () => {\n  let test = createReporter()\n  test.app.type('A', {\n    access: () => true,\n    process: () => {}\n  })\n  test.app.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n  await test.app.log.add(\n    { type: 'A' },\n    { channel: 'a', client: '1:1', node: '1:1:1', user: '1' }\n  )\n  expect(test.app.log.entries()).toEqual([\n    [\n      { type: 'A' },\n      {\n        added: 1,\n        channels: ['a'],\n        clients: ['1:1'],\n        id: '1 server:uuid 0',\n        nodes: ['1:1:1'],\n        reasons: ['test'],\n        server: 'server:uuid',\n        status: 'waiting',\n        subprotocol: 0,\n        time: 1,\n        users: ['1']\n      }\n    ]\n  ])\n  await setTimeout(10)\n  expect(test.app.log.entries()).toEqual([\n    [\n      { type: 'A' },\n      {\n        added: 1,\n        channels: ['a'],\n        clients: ['1:1'],\n        id: '1 server:uuid 0',\n        nodes: ['1:1:1'],\n        reasons: ['test'],\n        server: 'server:uuid',\n        status: 'processed',\n        subprotocol: 0,\n        time: 1,\n        users: ['1']\n      }\n    ]\n  ])\n})\n\nit('tracks action processing on add', async () => {\n  let error = new Error('test')\n  let app = createServer()\n  app.type('FOO', {\n    access: () => false,\n    resend: () => ({ channels: ['foo'] })\n  })\n  app.type('ERROR', {\n    access: () => false,\n    process() {\n      throw error\n    }\n  })\n\n  let meta = await app.process({ type: 'FOO' }, { a: 1 })\n  expect(meta.a).toEqual(1)\n  expect(meta.channels).toEqual(['foo'])\n\n  let err\n  try {\n    await app.process({ type: 'ERROR' })\n  } catch (e) {\n    err = e\n  }\n  expect(err).toBe(error)\n})\n\nit('has shortcut API for action creators', async () => {\n  type ActionA = { aValue: string; type: 'A' }\n  let createA = defineAction<ActionA>('A')\n\n  let processed: string[] = []\n  let app = createServer()\n  app.type(createA, {\n    access: () => true,\n    process(ctx, action) {\n      processed.push(action.aValue)\n    }\n  })\n\n  await app.process(createA({ aValue: 'test' }))\n  expect(processed).toEqual(['test'])\n})\n\nit('has alias to root from file URL', () => {\n  let app = new BaseServer({\n    fileUrl: import.meta.url,\n    minSubprotocol: 1,\n    subprotocol: 1\n  })\n  expect(app.options.root).toEqual(import.meta.dirname)\n})\n\nit('has custom logger', () => {\n  let app = new BaseServer({\n    minSubprotocol: 1,\n    root: import.meta.dirname,\n    subprotocol: 1\n  })\n  spyOn(console, 'warn', () => {})\n  app.logger.warn({ test: 1 }, 'test')\n  expect(calls(console.warn)).toEqual([[{ test: 1 }, 'test']])\n})\n\nit('subscribes clients manually', async () => {\n  let app = new BaseServer({\n    minSubprotocol: 1,\n    root: import.meta.dirname,\n    subprotocol: 1\n  })\n  let actions: Action[] = []\n  app.log.on('add', (action, meta) => {\n    expect(meta.nodes).toEqual(['test:1:1'])\n    actions.push(action)\n  })\n\n  app.subscribe('test:1:1', 'users/10')\n  await setTimeout(10)\n  expect(app.subscribers).toEqual({\n    'users/10': {\n      'test:1:1': { filters: { '{}': true } }\n    }\n  })\n  expect(actions).toEqual([{ channel: 'users/10', type: 'logux/subscribed' }])\n\n  app.subscribe('test:1:1', 'users/10')\n  await setTimeout(10)\n  expect(actions).toEqual([{ channel: 'users/10', type: 'logux/subscribed' }])\n})\n\nit('processes action with accessAndProcess callback', async () => {\n  let test = createReporter()\n  let accessAndProcess = spy()\n  test.app.type('A', {\n    accessAndProcess\n  })\n  await test.app.process({ type: 'A' })\n  expect(accessAndProcess.callCount).toEqual(1)\n})\n"
  },
  {
    "path": "context/index.d.ts",
    "content": "import type { AnyAction } from '@logux/core'\n\nimport type { ServerMeta } from '../base-server/index.js'\nimport type { ServerClient } from '../server-client/index.js'\nimport type { Server } from '../server/index.js'\n\nexport class ConnectContext<Headers extends object = unknown> {\n  /**\n   * Unique persistence client ID.\n   *\n   * ```js\n   * server.clientIds.get(node.clientId)\n   * ```\n   */\n  clientId: string\n\n  /**\n   * Client’s headers.\n   *\n   * ```js\n   * ctx.sendBack({\n   *   type: 'error',\n   *   message: I18n[ctx.headers.locale || 'en'].error\n   * })\n   * ```\n   */\n  headers: Headers\n\n  /**\n   * Unique node ID.\n   *\n   * ```js\n   * server.nodeIds.get(node.nodeId)\n   * ```\n   */\n  nodeId: string\n\n  /**\n   * Logux server\n   */\n  server: Server\n\n  /**\n   * Action creator application subprotocol version.\n   */\n  subprotocol: number\n\n  /**\n   * User ID taken node ID.\n   *\n   * ```js\n   * async access (ctx, action, meta) {\n   *   const user = await db.getUser(ctx.userId)\n   *   return user.admin\n   * }\n   * ```\n   */\n  userId: string\n\n  constructor(server: Server, client: ServerClient)\n\n  /**\n   * Send action back to the client.\n   *\n   * ```js\n   * ctx.sendBack({ type: 'login/success', token })\n   * ```\n   *\n   * Action will not be processed by server’s callbacks from `Server#type`.\n   *\n   * @param action The action.\n   * @param meta Action’s meta.\n   * @returns Promise until action was added to the server log.\n   */\n  sendBack(action: AnyAction, meta?: Partial<ServerMeta>): Promise<void>\n}\n\n/**\n * Action context.\n * ```\n */\nexport class Context<\n  Data extends object = unknown,\n  Headers extends object = unknown\n> extends ConnectContext<Headers> {\n  /**\n   * Open structure to save some data between different steps of processing.\n   *\n   * ```js\n   * server.type('RENAME', {\n   *   access (ctx, action, meta) {\n   *     ctx.data.user = findUser(ctx.userId)\n   *     return ctx.data.user.hasAccess(action.projectId)\n   *   }\n   *   process (ctx, action, meta) {\n   *     return ctx.data.user.rename(action.projectId, action.name)\n   *   }\n   * })\n   * ```\n   */\n  data: Data\n\n  /**\n   * Was action created by Logux server.\n   *\n   * ```js\n   * access: (ctx, action, meta) => ctx.isServer\n   * ```\n   */\n  isServer: boolean\n\n  constructor(server: Server, meta: ServerMeta)\n}\n\n/**\n * Subscription context.\n *\n * ```js\n * server.channel('user/:id', {\n *   access (ctx, action, meta) {\n *     return ctx.params.id === ctx.userId\n *   }\n * })\n * ```\n */\nexport class ChannelContext<\n  Data extends object,\n  ChannelParams extends object | string[],\n  Headers extends object\n> extends Context<Data, Headers> {\n  /**\n   * Parsed variable parts of channel pattern.\n   *\n   * ```js\n   * server.channel('user/:id', {\n   *   access (ctx, action, meta) {\n   *     action.channel //=> user/10\n   *     ctx.params //=> { id: '10' }\n   *   }\n   * })\n   * server.channel(/post\\/(\\d+)/, {\n   *   access (ctx, action, meta) {\n   *     action.channel //=> post/10\n   *     ctx.params //=> ['post/10', '10']\n   *   }\n   * })\n   * ```\n   */\n  params: ChannelParams\n}\n"
  },
  {
    "path": "context/index.js",
    "content": "import { parseId } from '@logux/core'\n\nexport class Context {\n  constructor(server, meta) {\n    this.server = server\n    this.data = {}\n\n    let client\n    if (meta.node) {\n      client = meta\n      this.nodeId = client.nodeId\n      this.userId = client.userId\n      this.clientId = client.clientId\n      this.subprotocol = client.node.remoteSubprotocol\n    } else {\n      let parsed = parseId(meta.id)\n      this.nodeId = parsed.nodeId\n      this.userId = parsed.userId\n      this.clientId = parsed.clientId\n      this.isServer = this.userId === 'server'\n      client = server.clientIds.get(this.clientId)\n      if (meta.subprotocol) {\n        this.subprotocol = meta.subprotocol\n      } else if (client) {\n        this.subprotocol = client.node.remoteSubprotocol\n      }\n    }\n\n    if (client) {\n      this.headers = client.node.remoteHeaders\n    } else {\n      this.headers = {}\n    }\n  }\n\n  sendBack(action, meta = {}) {\n    return this.server.log.add(action, {\n      clients: [this.clientId],\n      status: 'processed',\n      ...meta\n    })\n  }\n}\n"
  },
  {
    "path": "context/index.test.ts",
    "content": "import type { Action } from '@logux/core'\nimport { beforeEach, expect, it } from 'vitest'\n\nimport { Context, type ServerMeta } from '../index.js'\n\nlet added: [Action, ServerMeta][] = []\n\nconst FAKE_SERVER: any = {\n  clientIds: new Map([\n    [\n      '20:client',\n      { node: { remoteHeaders: { locale: 'fr' }, remoteSubprotocol: 2 } }\n    ]\n  ]),\n\n  log: {\n    add(action: Action, meta: ServerMeta) {\n      added.push([action, meta])\n      return Promise.resolve()\n    }\n  }\n}\n\nbeforeEach(() => {\n  added = []\n})\n\nfunction createContext(\n  meta: Partial<ServerMeta> = { id: '1 10:client:uuid 0', subprotocol: 1 }\n): Context {\n  return new Context(FAKE_SERVER, meta as ServerMeta)\n}\n\nit('has open data', () => {\n  let ctx = createContext()\n  expect(ctx.data).toEqual({})\n})\n\nit('parses meta', () => {\n  let ctx = createContext()\n  expect(ctx.nodeId).toEqual('10:client:uuid')\n  expect(ctx.clientId).toEqual('10:client')\n  expect(ctx.userId).toEqual('10')\n  expect(ctx.subprotocol).toEqual(1)\n})\n\nit('detects servers', () => {\n  let user = createContext({ id: '1 10:uuid 0' })\n  expect(user.isServer).toBe(false)\n  let server = createContext({ id: '1 server:uuid 0' })\n  expect(server.isServer).toBe(true)\n})\n\nit('takes subprotocol from client', () => {\n  let ctx = createContext({ id: '1 20:client:uuid 0' })\n  expect(ctx.subprotocol).toEqual(2)\n})\n\nit('works on missed subprotocol', () => {\n  let ctx = createContext({ id: '1 10:client:uuid 0' })\n  expect(ctx.subprotocol).toBeUndefined()\n})\n\nit('takes headers from client', () => {\n  let ctx = createContext({ id: '1 20:client:uuid 0' })\n  expect(ctx.headers).toEqual({ locale: 'fr' })\n})\n\nit('works on missed headers', () => {\n  let ctx = createContext({ id: '1 10:client:uuid 0' })\n  expect(ctx.headers).toEqual({})\n})\n\nit('sends action back', () => {\n  let ctx = createContext()\n  expect(ctx.sendBack({ type: 'A' }) instanceof Promise).toBe(true)\n  ctx.sendBack({ type: 'B' }, { clients: [], reasons: ['1'] })\n  expect(added).toEqual([\n    [{ type: 'A' }, { clients: ['10:client'], status: 'processed' }],\n    [{ type: 'B' }, { clients: [], reasons: ['1'], status: 'processed' }]\n  ])\n})\n"
  },
  {
    "path": "create-http-server/index.js",
    "content": "import { promises as fs } from 'node:fs'\nimport http from 'node:http'\nimport https from 'node:https'\nimport { isAbsolute, join } from 'node:path'\n\nconst PEM_PREAMBLE = '-----BEGIN'\n\nfunction isPem(content) {\n  if (typeof content === 'object' && content.pem) {\n    return true\n  } else {\n    return content.toString().trim().startsWith(PEM_PREAMBLE)\n  }\n}\n\nfunction readFrom(root, file) {\n  file = file.toString()\n  if (!isAbsolute(file)) file = join(root, file)\n  return fs.readFile(file)\n}\n\nexport async function createHttpServer(opts) {\n  let server\n  if (opts.server) {\n    server = opts.server\n  } else {\n    let key = opts.key\n    let cert = opts.cert\n    if (key && !isPem(key)) key = await readFrom(opts.root, key)\n    if (cert && !isPem(cert)) cert = await readFrom(opts.root, cert)\n\n    if (key && key.pem) {\n      server = https.createServer({ cert, key: key.pem })\n    } else if (key) {\n      server = https.createServer({ cert, key })\n    } else {\n      server = http.createServer()\n    }\n  }\n\n  return server\n}\n"
  },
  {
    "path": "create-reporter/__snapshots__/index.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`allows custom loggers 1`] = `\"{\"details\":{\"connectionId\":\"670\",\"ipAddress\":\"10.110.6.56\"},\"msg\":\"Client was connected\"}\"`;\n\nexports[`reports EACCES error 1`] = `\n\"{\"level\":60,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"err\":{},\"note\":\"Non-privileged users can't start a listening socket on ports below 1024. Try to change user or take another port.\\\\n\\\\n$ su - \\`<username>\\`\\\\n$ npm start -p 80\",\"msg\":\"You are not allowed to run server on port \\`80\\`\"}\nFLUSH\"\n`;\n\nexports[`reports EACCES error 2`] = `\n\"\u001b[41m\u001b[37m FATAL \u001b[39m\u001b[49m \u001b[1m\u001b[31mYou are not allowed to run server on port \u001b[33m80\u001b[39m\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        \u001b[90mNon-privileged users can't start a listening socket on ports below 1024.\u001b[39m\n        \u001b[90mTry to change user or take another port.\u001b[39m\n        \u001b[90m\u001b[39m\n        \u001b[90m$ su - \u001b[1m<username>\u001b[22m\u001b[39m\n        \u001b[90m$ npm start -p 80\u001b[39m\n\n\"\n`;\n\nexports[`reports actions with excludeClients metadata 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"action\":{\"id\":100,\"name\":\"John\",\"type\":\"ADD_USER\"},\"meta\":{\"excludeClients\":[\"1:-lCr7e9s\",\"2:wv0r_O5C\"],\"id\":\"1487805099387 100:uImkcF4z 0\",\"reasons\":[],\"server\":\"server:H1f8LAyzl\",\"subprotocol\":1,\"time\":1487805099387},\"msg\":\"Action was added\"}\n\"\n`;\n\nexports[`reports actions with excludeClients metadata 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mAction was added\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action: \n          id:   \u001b[1m100\u001b[22m\n          name: \"\u001b[1mJohn\u001b[22m\"\n          type: \"\u001b[1mADD_USER\u001b[22m\"\n        Meta:   \n          excludeClients: [\"\u001b[1m1\u001b[22m:\u001b[33m-lC\u001b[39m\u001b[35mr7e\u001b[39m\u001b[31m9s\u001b[39m\",\"\u001b[1m2\u001b[22m:\u001b[33mwv0\u001b[39m\u001b[31mr_O\u001b[39m\u001b[34m5C\u001b[39m\"]\n          id:             \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n          reasons:        []\n          server:         \u001b[1mserver\u001b[22m:\u001b[34mH1f\u001b[39m\u001b[35m8LA\u001b[39m\u001b[36myzl\u001b[39m\n          subprotocol:    \u001b[1m1\u001b[22m\n          time:           \u001b[1m1487805099387\u001b[22m\n\n\"\n`;\n\nexports[`reports actions with metadata containing 'clients' array 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"action\":{\"id\":100,\"name\":\"John\",\"type\":\"ADD_USER\"},\"meta\":{\"clients\":[\"1:-lCr7e9s\",\"2:wv0r_O5C\"],\"id\":\"1487805099387 100:uImkcF4z 0\",\"reasons\":[],\"server\":\"server:H1f8LAyzl\",\"subprotocol\":1,\"time\":1487805099387},\"msg\":\"Action was added\"}\n\"\n`;\n\nexports[`reports actions with metadata containing 'clients' array 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mAction was added\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action: \n          id:   \u001b[1m100\u001b[22m\n          name: \"\u001b[1mJohn\u001b[22m\"\n          type: \"\u001b[1mADD_USER\u001b[22m\"\n        Meta:   \n          clients:     [\"\u001b[1m1\u001b[22m:\u001b[33m-lC\u001b[39m\u001b[35mr7e\u001b[39m\u001b[31m9s\u001b[39m\",\"\u001b[1m2\u001b[22m:\u001b[33mwv0\u001b[39m\u001b[31mr_O\u001b[39m\u001b[34m5C\u001b[39m\"]\n          id:          \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n          reasons:     []\n          server:      \u001b[1mserver\u001b[22m:\u001b[34mH1f\u001b[39m\u001b[35m8LA\u001b[39m\u001b[36myzl\u001b[39m\n          subprotocol: \u001b[1m1\u001b[22m\n          time:        \u001b[1m1487805099387\u001b[22m\n\n\"\n`;\n\nexports[`reports add 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"action\":{\"data\":{\"array\":[1,[2],{\"a\":\"1\",\"b\":{\"c\":2},\"d\":[],\"e\":null},null],\"name\":\"John\",\"role\":null},\"id\":100,\"type\":\"CHANGE_USER\"},\"meta\":{\"id\":\"1487805099387 100:uImkcF4z 0\",\"reasons\":[\"lastValue\",\"debug\"],\"server\":\"server:H1f8LAyzl\",\"subprotocol\":1,\"time\":1487805099387},\"msg\":\"Action was added\"}\n\"\n`;\n\nexports[`reports add 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mAction was added\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action: \n          data: \n            array: [\u001b[1m1\u001b[22m, [\u001b[1m2\u001b[22m], { a: \"\u001b[1m1\u001b[22m\", b: { c: \u001b[1m2\u001b[22m }, d: [], e: \u001b[1mnull\u001b[22m }, \u001b[1mnull\u001b[22m]\n            name:  \"\u001b[1mJohn\u001b[22m\"\n            role:  \u001b[1mnull\u001b[22m\n          id:   \u001b[1m100\u001b[22m\n          type: \"\u001b[1mCHANGE_USER\u001b[22m\"\n        Meta:   \n          id:          \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n          reasons:     [\"\u001b[1mlastValue\u001b[22m\", \"\u001b[1mdebug\u001b[22m\"]\n          server:      \u001b[1mserver\u001b[22m:\u001b[34mH1f\u001b[39m\u001b[35m8LA\u001b[39m\u001b[36myzl\u001b[39m\n          subprotocol: \u001b[1m1\u001b[22m\n          time:        \u001b[1m1487805099387\u001b[22m\n\n\"\n`;\n\nexports[`reports add and clean 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"action\":{\"data\":{\"array\":[1,[2],{\"a\":\"1\",\"b\":{\"c\":2},\"d\":[],\"e\":null},null],\"name\":\"John\",\"role\":null},\"id\":100,\"type\":\"CHANGE_USER\"},\"meta\":{\"id\":\"1487805099387 100:uImkcF4z 0\",\"reasons\":[\"lastValue\",\"debug\"],\"server\":\"server:H1f8LAyzl\",\"subprotocol\":1,\"time\":1487805099387},\"msg\":\"Action was added and cleaned\"}\n\"\n`;\n\nexports[`reports add and clean 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mAction was added and cleaned\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action: \n          data: \n            array: [\u001b[1m1\u001b[22m, [\u001b[1m2\u001b[22m], { a: \"\u001b[1m1\u001b[22m\", b: { c: \u001b[1m2\u001b[22m }, d: [], e: \u001b[1mnull\u001b[22m }, \u001b[1mnull\u001b[22m]\n            name:  \"\u001b[1mJohn\u001b[22m\"\n            role:  \u001b[1mnull\u001b[22m\n          id:   \u001b[1m100\u001b[22m\n          type: \"\u001b[1mCHANGE_USER\u001b[22m\"\n        Meta:   \n          id:          \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n          reasons:     [\"\u001b[1mlastValue\u001b[22m\", \"\u001b[1mdebug\u001b[22m\"]\n          server:      \u001b[1mserver\u001b[22m:\u001b[34mH1f\u001b[39m\u001b[35m8LA\u001b[39m\u001b[36myzl\u001b[39m\n          subprotocol: \u001b[1m1\u001b[22m\n          time:        \u001b[1m1487805099387\u001b[22m\n\n\"\n`;\n\nexports[`reports authenticated 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"connectionId\":\"670\",\"nodeId\":\"admin:100:uImkcF4z\",\"subprotocol\":1,\"msg\":\"User was authenticated\"}\n\"\n`;\n\nexports[`reports authenticated 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mUser was authenticated\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n        Node ID:       \u001b[1madmin\u001b[22m:\u001b[31m100\u001b[39m\n        Subprotocol:   \u001b[1m1\u001b[22m\n\n\"\n`;\n\nexports[`reports authenticated without user ID 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"connectionId\":\"670\",\"nodeId\":\"uImkcF4z\",\"subprotocol\":1,\"msg\":\"User was authenticated\"}\n\"\n`;\n\nexports[`reports authenticated without user ID 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mUser was authenticated\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n        Node ID:       uImkcF4z\n        Subprotocol:   \u001b[1m1\u001b[22m\n\n\"\n`;\n\nexports[`reports clean 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1487805099387 100:uImkcF4z 0\",\"msg\":\"Action was cleaned\"}\n\"\n`;\n\nexports[`reports clean 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mAction was cleaned\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n\n\"\n`;\n\nexports[`reports connect 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"connectionId\":\"670\",\"ipAddress\":\"10.110.6.56\",\"msg\":\"Client was connected\"}\n\"\n`;\n\nexports[`reports connect 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mClient was connected\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n        IP address:    \u001b[1m10.110.6.56\u001b[22m\n\n\"\n`;\n\nexports[`reports denied 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1487805099387 100:uImkcF4z 0\",\"msg\":\"Action was denied\"}\n\"\n`;\n\nexports[`reports denied 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mAction was denied\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n\n\"\n`;\n\nexports[`reports destroy 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"msg\":\"Shutting down Logux server\"}\n\"\n`;\n\nexports[`reports destroy 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mShutting down Logux server\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n\n\"\n`;\n\nexports[`reports disconnect 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"nodeId\":\"100:uImkcF4z\",\"msg\":\"Client was disconnected\"}\n\"\n`;\n\nexports[`reports disconnect 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mClient was disconnected\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Node ID: \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m\n\n\"\n`;\n\nexports[`reports disconnect from unauthenticated user 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"connectionId\":\"670\",\"msg\":\"Client was disconnected\"}\n\"\n`;\n\nexports[`reports disconnect from unauthenticated user 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mClient was disconnected\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n\n\"\n`;\n\nexports[`reports error 1`] = `\n\"{\"level\":60,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"err\":{\"message\":\"Some mistake\",\"name\":\"Error\",\"stack\":\"Error: Some mistake\\\\n    at Object.<anonymous> (/dev/app/index.js:28:13)\\\\n    at Module._compile (module.js:573:32)\\\\n    at at runTest (/dev/app/node_modules/jest/index.js:50:10)\\\\n    at process._tickCallback (internal/process/next_tick.js:103:7)\"},\"msg\":\"Some mistake\"}\nFLUSH\"\n`;\n\nexports[`reports error 2`] = `\n\"\u001b[41m\u001b[37m FATAL \u001b[39m\u001b[49m \u001b[1m\u001b[31mSome mistake\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n\u001b[90m        at Object.<anonymous> (/dev/app/index.js:28:13)\u001b[39m\n\u001b[90m        at Module._compile (module.js:573:32)\u001b[39m\n\u001b[90m        at at runTest (/dev/app/node_modules/jest/index.js:50:10)\u001b[39m\n\u001b[90m        at process._tickCallback (internal/process/next_tick.js:103:7)\u001b[39m\n\n\"\n`;\n\nexports[`reports error from action 1`] = `\n\"{\"level\":50,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"err\":{\"message\":\"Some mistake\",\"name\":\"Error\",\"stack\":\"Error: Some mistake\\\\n    at Object.<anonymous> (/dev/app/index.js:28:13)\\\\n    at Module._compile (module.js:573:32)\\\\n    at at runTest (/dev/app/node_modules/jest/index.js:50:10)\\\\n    at process._tickCallback (internal/process/next_tick.js:103:7)\"},\"actionId\":\"1487805099387 100:uImkcF4z 0\",\"msg\":\"Some mistake\"}\n\"\n`;\n\nexports[`reports error from action 2`] = `\n\"\u001b[41m\u001b[37m ERROR \u001b[39m\u001b[49m \u001b[1m\u001b[31mSome mistake\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n\u001b[90m        at Object.<anonymous> (/dev/app/index.js:28:13)\u001b[39m\n\u001b[90m        at Module._compile (module.js:573:32)\u001b[39m\n\u001b[90m        at at runTest (/dev/app/node_modules/jest/index.js:50:10)\u001b[39m\n\u001b[90m        at process._tickCallback (internal/process/next_tick.js:103:7)\u001b[39m\n        Action ID: \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n\n\"\n`;\n\nexports[`reports error from client 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"connectionId\":\"670\",\"msg\":\"Client error: A timeout was reached (5000 ms)\"}\n\"\n`;\n\nexports[`reports error from client 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mClient error: A timeout was reached (5000 ms)\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n\n\"\n`;\n\nexports[`reports error from node 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"nodeId\":\"100:uImkcF4z\",\"msg\":\"Sync error: A timeout was reached (5000 ms)\"}\n\"\n`;\n\nexports[`reports error from node 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mSync error: A timeout was reached (5000 ms)\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Node ID: \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m\n\n\"\n`;\n\nexports[`reports error with token 1`] = `\n\"{\"level\":50,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"err\":{\"message\":\"{\\\\\"Authorization\\\\\":\\\\\"[SECRET]\\\\\"}\",\"name\":\"Error\",\"stack\":\"Error: {\\\\\"Authorization\\\\\":\\\\\"[SECRET]\\\\\"}\\\\n    at Object.<anonymous> (/dev/app/index.js:28:13)\\\\n    at Module._compile (module.js:573:32)\\\\n    at at runTest (/dev/app/node_modules/jest/index.js:50:10)\\\\n    at process._tickCallback (internal/process/next_tick.js:103:7)\"},\"actionId\":\"1487805099387 100:uImkcF4z 0\",\"msg\":\"{\\\\\"Authorization\\\\\":\\\\\"[SECRET]\\\\\"}\"}\n\"\n`;\n\nexports[`reports error with token 2`] = `\n\"\u001b[41m\u001b[37m ERROR \u001b[39m\u001b[49m \u001b[1m\u001b[31m{\"Authorization\":\"[SECRET]\"}\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n\u001b[90m        at Object.<anonymous> (/dev/app/index.js:28:13)\u001b[39m\n\u001b[90m        at Module._compile (module.js:573:32)\u001b[39m\n\u001b[90m        at at runTest (/dev/app/node_modules/jest/index.js:50:10)\u001b[39m\n\u001b[90m        at process._tickCallback (internal/process/next_tick.js:103:7)\u001b[39m\n        Action ID: \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n\n\"\n`;\n\nexports[`reports listen 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"environment\":\"development\",\"loguxServer\":\"0.0.0\",\"minSubprotocol\":0,\"nodeId\":\"server:FnXaqDxY\",\"subprotocol\":0,\"note\":[\"Server was started in non-secure development mode\",\"Press Ctrl-C to shutdown server\"],\"listen\":\"ws://127.0.0.1:31337/\",\"healthCheck\":\"http://127.0.0.1:31337/health\",\"msg\":\"Logux server is listening\"}\n\"\n`;\n\nexports[`reports listen 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mLogux server is listening\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        PID:             \u001b[1m21384\u001b[22m\n        Environment:     \u001b[1mdevelopment\u001b[22m\n        Logux server:    \u001b[1m0.0.0\u001b[22m\n        Min subprotocol: \u001b[1m0\u001b[22m\n        Node ID:         \u001b[1mserver\u001b[22m:\u001b[31mFnX\u001b[39m\u001b[35maqD\u001b[39m\u001b[34mxY\u001b[39m\n        Subprotocol:     \u001b[1m0\u001b[22m\n        Health check:    \u001b[1mhttp://127.0.0.1:31337/health\u001b[22m\n        Listen:          \u001b[1mws://127.0.0.1:31337/\u001b[22m\n        \u001b[90mServer was started in non-secure development mode\u001b[39m\n        \u001b[90mPress Ctrl-C to shutdown server\u001b[39m\n\n\"\n`;\n\nexports[`reports listen for custom domain 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"environment\":\"development\",\"loguxServer\":\"0.0.0\",\"minSubprotocol\":0,\"nodeId\":\"server:FnXaqDxY\",\"subprotocol\":0,\"note\":[\"Server was started in non-secure development mode\",\"Press Ctrl-C to shutdown server\"],\"server\":true,\"prometheus\":\"http://127.0.0.1:31338/prometheus\",\"msg\":\"Logux server is listening\"}\n\"\n`;\n\nexports[`reports listen for custom domain 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mLogux server is listening\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        PID:             \u001b[1m21384\u001b[22m\n        Environment:     \u001b[1mdevelopment\u001b[22m\n        Logux server:    \u001b[1m0.0.0\u001b[22m\n        Min subprotocol: \u001b[1m0\u001b[22m\n        Node ID:         \u001b[1mserver\u001b[22m:\u001b[31mFnX\u001b[39m\u001b[35maqD\u001b[39m\u001b[34mxY\u001b[39m\n        Subprotocol:     \u001b[1m0\u001b[22m\n        Prometheus:      \u001b[1mhttp://127.0.0.1:31338/prometheus\u001b[22m\n        Listen:          \u001b[1mCustom HTTP server\u001b[22m\n        \u001b[90mServer was started in non-secure development mode\u001b[39m\n        \u001b[90mPress Ctrl-C to shutdown server\u001b[39m\n\n\"\n`;\n\nexports[`reports listen for production 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"environment\":\"production\",\"loguxServer\":\"0.0.0\",\"minSubprotocol\":0,\"nodeId\":\"server:FnXaqDxY\",\"subprotocol\":0,\"listen\":\"wss://127.0.0.1:31337/\",\"healthCheck\":\"https://127.0.0.1:31337/health\",\"redis\":\"//localhost\",\"msg\":\"Logux server is listening\"}\n\"\n`;\n\nexports[`reports listen for production 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mLogux server is listening\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        PID:             \u001b[1m21384\u001b[22m\n        Environment:     \u001b[1mproduction\u001b[22m\n        Logux server:    \u001b[1m0.0.0\u001b[22m\n        Min subprotocol: \u001b[1m0\u001b[22m\n        Node ID:         \u001b[1mserver\u001b[22m:\u001b[31mFnX\u001b[39m\u001b[35maqD\u001b[39m\u001b[34mxY\u001b[39m\n        Subprotocol:     \u001b[1m0\u001b[22m\n        Health check:    \u001b[1mhttps://127.0.0.1:31337/health\u001b[22m\n        Redis:           \u001b[1m//localhost\u001b[22m\n        Listen:          \u001b[1mwss://127.0.0.1:31337/\u001b[22m\n\n\"\n`;\n\nexports[`reports subscribed 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1487805099387 100:uImkcF4z 0\",\"channel\":\"user/100\",\"msg\":\"Client was subscribed\"}\n\"\n`;\n\nexports[`reports subscribed 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mClient was subscribed\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n        Channel:   \u001b[1muser/100\u001b[22m\n\n\"\n`;\n\nexports[`reports unauthenticated 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"connectionId\":\"670\",\"nodeId\":\"100:uImkcF4z\",\"subprotocol\":1,\"msg\":\"Bad authentication\"}\n\"\n`;\n\nexports[`reports unauthenticated 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mBad authentication\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n        Node ID:       \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m\n        Subprotocol:   \u001b[1m1\u001b[22m\n\n\"\n`;\n\nexports[`reports unknownType 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1487805099387 100:vAApgNT9 0\",\"type\":\"CHANGE_SER\",\"msg\":\"Action with unknown type\"}\n\"\n`;\n\nexports[`reports unknownType 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mAction with unknown type\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[34mvAA\u001b[39m\u001b[32mpgN\u001b[39m\u001b[31mT9\u001b[39m \u001b[1m0\u001b[22m\n        Type:      \u001b[1mCHANGE_SER\u001b[22m\n\n\"\n`;\n\nexports[`reports unknownType from server 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1650269021700 server:FnXaqDxY 0\",\"type\":\"CHANGE_SER\",\"msg\":\"Action with unknown type\"}\n\"\n`;\n\nexports[`reports unknownType from server 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mAction with unknown type\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[34m1650\u001b[39m\u001b[35m2690\u001b[39m\u001b[31m2170\u001b[39m\u001b[31m0\u001b[39m\u001b[22m \u001b[1mserver\u001b[22m:\u001b[31mFnX\u001b[39m\u001b[35maqD\u001b[39m\u001b[34mxY\u001b[39m \u001b[1m0\u001b[22m\n        Type:      \u001b[1mCHANGE_SER\u001b[22m\n\n\"\n`;\n\nexports[`reports unsubscribed 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1650271940900 100:uImkcF4z 0\",\"channel\":\"user/100\",\"msg\":\"Client was unsubscribed\"}\n\"\n`;\n\nexports[`reports unsubscribed 2`] = `\n\"\u001b[42m\u001b[30m INFO \u001b[39m\u001b[49m  \u001b[1m\u001b[32mClient was unsubscribed\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[36m1650\u001b[39m\u001b[34m2719\u001b[39m\u001b[35m4090\u001b[39m\u001b[35m0\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n        Channel:   \u001b[1muser/100\u001b[22m\n\n\"\n`;\n\nexports[`reports useless actions 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"action\":{\"id\":100,\"name\":\"John\",\"type\":\"ADD_USER\"},\"meta\":{\"id\":\"1487805099387 100:uImkcF4z 0\",\"reasons\":[],\"server\":\"server:H1f8LAyzl\",\"subprotocol\":1,\"time\":1487805099387},\"msg\":\"Useless action\"}\n\"\n`;\n\nexports[`reports useless actions 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mUseless action\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action: \n          id:   \u001b[1m100\u001b[22m\n          name: \"\u001b[1mJohn\u001b[22m\"\n          type: \"\u001b[1mADD_USER\u001b[22m\"\n        Meta:   \n          id:          \u001b[1m\u001b[32m1487\u001b[39m\u001b[35m8050\u001b[39m\u001b[33m9938\u001b[39m\u001b[33m7\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n          reasons:     []\n          server:      \u001b[1mserver\u001b[22m:\u001b[34mH1f\u001b[39m\u001b[35m8LA\u001b[39m\u001b[36myzl\u001b[39m\n          subprotocol: \u001b[1m1\u001b[22m\n          time:        \u001b[1m1487805099387\u001b[22m\n\n\"\n`;\n\nexports[`reports wrongChannel 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1650269045800 100:IsvVzqWx 0\",\"channel\":\"ser/100\",\"msg\":\"Wrong channel name\"}\n\"\n`;\n\nexports[`reports wrongChannel 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mWrong channel name\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[32m1650\u001b[39m\u001b[34m2690\u001b[39m\u001b[33m4580\u001b[39m\u001b[33m0\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[31mIsv\u001b[39m\u001b[33mVzq\u001b[39m\u001b[34mWx\u001b[39m \u001b[1m0\u001b[22m\n        Channel:   \u001b[1mser/100\u001b[22m\n\n\"\n`;\n\nexports[`reports wrongChannel without name 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"actionId\":\"1650269056600 100:uImkcF4z 0\",\"msg\":\"Wrong channel name\"}\n\"\n`;\n\nexports[`reports wrongChannel without name 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mWrong channel name\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Action ID: \u001b[1m\u001b[36m1650\u001b[39m\u001b[31m2690\u001b[39m\u001b[34m5660\u001b[39m\u001b[34m0\u001b[39m\u001b[22m \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m \u001b[1m0\u001b[22m\n        Channel:   \u001b[1mundefined\u001b[22m\n\n\"\n`;\n\nexports[`reports zombie 1`] = `\n\"{\"level\":40,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"nodeId\":\"100:uImkcF4z\",\"msg\":\"Zombie client was disconnected\"}\n\"\n`;\n\nexports[`reports zombie 2`] = `\n\"\u001b[43m\u001b[30m WARN \u001b[39m\u001b[49m  \u001b[1m\u001b[33mZombie client was disconnected\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Node ID: \u001b[1m100\u001b[22m:\u001b[32muIm\u001b[39m\u001b[36mkcF\u001b[39m\u001b[33m4z\u001b[39m\n\n\"\n`;\n\nexports[`stacktrace > reports EADDRINUSE error 1`] = `\n\"{\"level\":60,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"err\":{},\"note\":\"Another Logux server or other app already running on this port. Probably you haven’t stopped server from other project or previous version of this server was not killed.\\\\n\\\\n$ su - root\\\\n# netstat -nlp | grep 31337\\\\nProto   Local Address   State    PID/Program name\\\\ntcp     0.0.0.0:31337   LISTEN   \\`777\\`/node\\\\n# sudo kill -9 \\`777\\`\",\"msg\":\"Port \\`31337\\` already in use\"}\nFLUSH\"\n`;\n\nexports[`stacktrace > reports EADDRINUSE error 2`] = `\n\"\u001b[41m\u001b[37m FATAL \u001b[39m\u001b[49m \u001b[1m\u001b[31mPort \u001b[33m31337\u001b[31m already in use\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        \u001b[90mAnother Logux server or other app already running on this port.\u001b[39m\n        \u001b[90mProbably you haven’t stopped server from other project or previous\u001b[39m\n        \u001b[90mversion of this server was not killed.\u001b[39m\n        \u001b[90m\u001b[39m\n        \u001b[90m$ su - root\u001b[39m\n        \u001b[90m# netstat -nlp | grep 31337\u001b[39m\n        \u001b[90mProto   Local Address   State    PID/Program name\u001b[39m\n        \u001b[90mtcp     0.0.0.0:31337   LISTEN   \u001b[1m777\u001b[22m/node\u001b[39m\n        \u001b[90m# sudo kill -9 \u001b[1m777\u001b[22m\u001b[39m\n\n\"\n`;\n\nexports[`stacktrace > reports Logux error 1`] = `\n\"{\"level\":60,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"note\":\"Maybe there is a mistake in option name or this version of Logux Server doesn’t support this option\",\"msg\":\"Unknown option \\`suprotocol\\` in server constructor\"}\nFLUSH\"\n`;\n\nexports[`stacktrace > reports Logux error 2`] = `\n\"\u001b[41m\u001b[37m FATAL \u001b[39m\u001b[49m \u001b[1m\u001b[31mUnknown option \u001b[33msuprotocol\u001b[31m in server constructor\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        \u001b[90mMaybe there is a mistake in option name or this version of Logux Server\u001b[39m\n        \u001b[90mdoesn’t support this option\u001b[39m\n\n\"\n`;\n\nexports[`stacktrace > reports sync error 1`] = `\n\"{\"level\":50,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"err\":{\"message\":\"Logux received unknown-message error (Unknown message \\`bad\\` type)\",\"name\":\"LoguxError\"},\"connectionId\":\"670\",\"msg\":\"Logux received unknown-message error (Unknown message \\`bad\\` type)\"}\n\"\n`;\n\nexports[`stacktrace > reports sync error 2`] = `\n\"\u001b[41m\u001b[37m ERROR \u001b[39m\u001b[49m \u001b[1m\u001b[31mLogux received unknown-message error (Unknown message \u001b[33mbad\u001b[31m type)\u001b[39m\u001b[22m \u001b[2mat 1970-01-01 00:00:00\u001b[22m\n        Connection ID: \u001b[1m670\u001b[22m\n\n\"\n`;\n"
  },
  {
    "path": "create-reporter/index.js",
    "content": "import os from 'node:os'\nimport { sep } from 'node:path'\n\nimport humanFormatter from '../human-formatter/index.js'\n\nconst ERROR_CODES = {\n  EACCES: (e, environment) => {\n    let wayToFix = {\n      development: 'In dev mode it can be done with sudo:\\n$ sudo npm start',\n      production: `$ su - \\`<username>\\`\\n$ npm start -p ${e.port}`\n    }\n\n    return {\n      msg: `You are not allowed to run server on port \\`${e.port}\\``,\n      note:\n        \"Non-privileged users can't start a listening socket on ports \" +\n        'below 1024. Try to change user or take another port.\\n\\n' +\n        (wayToFix[environment] || wayToFix.production)\n    }\n  },\n  EADDRINUSE: e => {\n    let port = String(e.port)\n    let wayToFix = {\n      darwin: `$ sudo lsof -i ${e.port}\\n$ sudo kill -9 \\`<processid>\\``,\n      linux:\n        '$ su - root\\n' +\n        `# netstat -nlp | grep ${e.port}\\n` +\n        'Proto   Local Address   State    PID/Program name\\n' +\n        `tcp     0.0.0.0:${port.padEnd(8)}LISTEN   \\`777\\`/node\\n` +\n        '# sudo kill -9 `777`',\n      win32:\n        'Run `cmd.exe` as an administrator\\n' +\n        'C:\\\\> netstat -a -b -n -o\\n' +\n        'C:\\\\> taskkill /F /PID `<processid>`'\n    }\n\n    return {\n      msg: `Port \\`${e.port}\\` already in use`,\n      note:\n        'Another Logux server or other app already running on this port. ' +\n        'Probably you haven’t stopped server from other project ' +\n        'or previous version of this server was not killed.\\n\\n' +\n        (wayToFix[os.platform()] || '')\n    }\n  }\n}\n\nconst REPORTERS = {\n  add: () => ({ msg: 'Action was added' }),\n\n  addClean: () => ({ msg: 'Action was added and cleaned' }),\n\n  authenticated: () => ({ msg: 'User was authenticated' }),\n\n  clean: () => ({ msg: 'Action was cleaned' }),\n\n  clientError: record => {\n    let result = {\n      details: {},\n      level: 'warn'\n    }\n    if (record.err.received) {\n      result.msg = `Client error: ${record.err.description}`\n    } else {\n      result.msg = `Sync error: ${record.err.description}`\n    }\n    for (let i in record) {\n      if (i !== 'err') {\n        result.details[i] = record[i]\n      }\n    }\n    return result\n  },\n\n  connect: () => ({ msg: 'Client was connected' }),\n\n  denied: () => ({ level: 'warn', msg: 'Action was denied' }),\n\n  destroy: () => ({ msg: 'Shutting down Logux server' }),\n\n  disconnect: () => ({ msg: 'Client was disconnected' }),\n\n  error: record => {\n    let result = {\n      details: {\n        err: {\n          message: record.err.message,\n          name: record.err.name,\n          stack: record.err.stack\n        }\n      },\n      level: record.fatal ? 'fatal' : 'error',\n      msg: record.err.message\n    }\n\n    let helper = ERROR_CODES[record.err.code]\n    if (helper) {\n      let help = helper(record.err, record.environment)\n      result.msg = help.msg\n      result.details.note = help.note\n      delete result.details.err.stack\n    } else if (record.err.logux) {\n      result.details.note = record.err.note\n      delete result.details.err\n    }\n\n    if (record.err.name === 'LoguxError') {\n      delete result.details.err.stack\n    }\n\n    for (let i in record) {\n      if (i !== 'err' && i !== 'fatal') {\n        result.details[i] = record[i]\n      }\n    }\n\n    return result\n  },\n\n  listen: r => {\n    let details = {\n      environment: r.environment,\n      loguxServer: r.loguxServer,\n      minSubprotocol: r.minSubprotocol,\n      nodeId: r.nodeId,\n      subprotocol: r.subprotocol\n    }\n\n    if (r.environment === 'development') {\n      details.note = [\n        'Server was started in non-secure development mode',\n        'Press Ctrl-C to shutdown server'\n      ]\n    }\n\n    if (r.server) {\n      details.server = r.server\n    } else {\n      let wsProtocol = r.cert ? 'wss://' : 'ws://'\n      let httpProtocol = r.cert ? 'https://' : 'http://'\n      details.listen = `${wsProtocol}${r.host}:${r.port}/`\n      details.healthCheck = `${httpProtocol}${r.host}:${r.port}/health`\n    }\n\n    if (r.redis) {\n      details.redis = r.redis\n    }\n\n    for (let i in r.notes) details[i] = r.notes[i]\n\n    return { details, msg: 'Logux server is listening' }\n  },\n\n  subscribed: () => ({ msg: 'Client was subscribed' }),\n\n  unauthenticated: () => ({ level: 'warn', msg: 'Bad authentication' }),\n\n  unknownType: record => ({\n    level: /^ server(:| )/.test(record.actionId) ? 'error' : 'warn',\n    msg: 'Action with unknown type'\n  }),\n\n  unsubscribed: () => ({ msg: 'Client was unsubscribed' }),\n\n  useless: () => ({ level: 'warn', msg: 'Useless action' }),\n\n  wrongChannel: () => ({\n    level: 'warn',\n    msg: 'Wrong channel name'\n  }),\n\n  zombie: () => ({ level: 'warn', msg: 'Zombie client was disconnected' })\n}\n\nfunction cleanFromKeys(obj, regexp, seen) {\n  let result = {}\n  for (let key in obj) {\n    let v = obj[key]\n    if (typeof v === 'string') {\n      result[key] = v.replace(regexp, '[SECRET]')\n    } else if (typeof v === 'object' && !Array.isArray(v) && v !== null) {\n      if (seen.includes(v)) {\n        throw new Error('Circular reference in action')\n      }\n      seen.push(v)\n      result[key] = cleanFromKeys(v, regexp, seen)\n      seen.pop()\n    } else {\n      result[key] = v\n    }\n  }\n  return result\n}\n\nfunction createRecord(level, details, msg) {\n  /* c8 ignore next 4 */\n  if (typeof details === 'string') {\n    msg = details\n    details = {}\n  }\n  return {\n    level,\n    time: new Date().toISOString(),\n    pid: process.pid,\n    ...details,\n    msg\n  }\n}\n\nexport function createReporter(options) {\n  let cleanFromLog = options.cleanFromLog || /Bearer [^\\s\"]+/g\n  function reporter(type, details) {\n    let report = REPORTERS[type](details)\n    let level = report.level || 'info'\n    let seen = []\n    reporter.logger[level](\n      cleanFromKeys(report.details || details || {}, cleanFromLog, seen),\n      report.msg.replace(cleanFromLog, '[SECRET]')\n    )\n  }\n\n  if (typeof options.logger !== 'string' && 'info' in options.logger) {\n    reporter.logger = options.logger\n  } else {\n    let format\n    if (options.logger === 'human' || options.logger.type === 'human') {\n      let basepath = options.root || process.cwd()\n      if (basepath.slice(-1) !== sep) basepath += sep\n      format = humanFormatter({ basepath })\n    } else {\n      format = record => JSON.stringify(record) + '\\n'\n    }\n    let stream = options.logger?.stream ?? process.stderr\n\n    reporter.logger = {\n      /* c8 ignore next 3 */\n      debug(details, msg) {\n        stream.write(format(createRecord(20, details, msg)))\n      },\n      info(details, msg) {\n        stream.write(format(createRecord(30, details, msg)))\n      },\n      warn(details, msg) {\n        stream.write(format(createRecord(40, details, msg)))\n      },\n      error(details, msg) {\n        stream.write(format(createRecord(50, details, msg)))\n      },\n      fatal(details, msg) {\n        if (stream.flushSync) {\n          stream.flushSync(format(createRecord(60, details, msg)))\n        } else {\n          stream.write(format(createRecord(60, details, msg)))\n        }\n      }\n    }\n  }\n  return reporter\n}\n"
  },
  {
    "path": "create-reporter/index.test.ts",
    "content": "import '../test/force-colors.js'\n\nimport { LoguxError } from '@logux/core'\nimport { describe, expect, it } from 'vitest'\n\nimport { createReporter } from './index.js'\n\nclass MemoryStream {\n  flushSync: ((chunk: string) => void) | undefined\n\n  string: string\n\n  constructor(flushSync: boolean) {\n    this.string = ''\n    if (flushSync) {\n      this.flushSync = chunk => {\n        this.string += chunk + 'FLUSH'\n      }\n    }\n  }\n\n  write(chunk: string): void {\n    this.string += chunk\n  }\n}\n\nfunction clean(str: string): string {\n  let cleaned = str\n    .replace(/\\r\\v/g, '\\n')\n    .replace(/\\d{4}-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d/g, '1970-01-01 00:00:00')\n    .replace(/\"time\":\"[^\"]+\"/g, '\"time\":\"1970-01-01T00:00:00.000Z\"')\n    .replace(/\"hostname\":\"[^\"]+\"/g, '\"hostname\":\"localhost\"')\n    .replace(/\"pid\":\\d+/g, '\"pid\":21384')\n    .replace(/PID:(\\s+.*m)\\d+(.*m)/, 'PID:$121384$2')\n  return cleaned\n}\n\nfunction check(type: string, details?: object): void {\n  let json = new MemoryStream(true)\n  let jsonReporter = createReporter({\n    logger: { stream: json, type: 'json' }\n  })\n  jsonReporter(type, details)\n  expect(clean(json.string)).toMatchSnapshot()\n\n  let human = new MemoryStream(false)\n  let humanReporter = createReporter({\n    logger: { stream: human, type: 'human' }\n  })\n  humanReporter(type, details)\n  expect(clean(human.string)).toMatchSnapshot()\n}\n\nfunction createError(name: string, message: string): Error {\n  let err = new Error(message)\n  err.name = name\n  err.stack =\n    `${name}: ${message}\\n` +\n    '    at Object.<anonymous> (/dev/app/index.js:28:13)\\n' +\n    '    at Module._compile (module.js:573:32)\\n' +\n    '    at at runTest (/dev/app/node_modules/jest/index.js:50:10)\\n' +\n    '    at process._tickCallback (internal/process/next_tick.js:103:7)'\n  return err\n}\n\nit('reports listen', () => {\n  check('listen', {\n    cert: false,\n    environment: 'development',\n    host: '127.0.0.1',\n    loguxServer: '0.0.0',\n    minSubprotocol: 0,\n    nodeId: 'server:FnXaqDxY',\n    notes: {},\n    port: 31337,\n    redis: undefined,\n    server: false,\n    subprotocol: 0\n  })\n})\n\nit('reports listen for production', () => {\n  check('listen', {\n    cert: true,\n    environment: 'production',\n    host: '127.0.0.1',\n    loguxServer: '0.0.0',\n    minSubprotocol: 0,\n    nodeId: 'server:FnXaqDxY',\n    notes: {},\n    port: 31337,\n    redis: '//localhost',\n    server: false,\n    subprotocol: 0\n  })\n})\n\nit('reports listen for custom domain', () => {\n  check('listen', {\n    environment: 'development',\n    loguxServer: '0.0.0',\n    minSubprotocol: 0,\n    nodeId: 'server:FnXaqDxY',\n    notes: {\n      prometheus: 'http://127.0.0.1:31338/prometheus'\n    },\n    server: true,\n    subprotocol: 0\n  })\n})\n\nit('reports connect', () => {\n  check('connect', { connectionId: '670', ipAddress: '10.110.6.56' })\n})\n\nit('reports authenticated', () => {\n  check('authenticated', {\n    connectionId: '670',\n    nodeId: 'admin:100:uImkcF4z',\n    subprotocol: 1\n  })\n})\n\nit('reports authenticated without user ID', () => {\n  check('authenticated', {\n    connectionId: '670',\n    nodeId: 'uImkcF4z',\n    subprotocol: 1\n  })\n})\n\nit('reports unauthenticated', () => {\n  check('unauthenticated', {\n    connectionId: '670',\n    nodeId: '100:uImkcF4z',\n    subprotocol: 1\n  })\n})\n\nit('reports add', () => {\n  check('add', {\n    action: {\n      data: {\n        array: [1, [2], { a: '1', b: { c: 2 }, d: [], e: null }, null],\n        name: 'John',\n        role: null\n      },\n      id: 100,\n      type: 'CHANGE_USER'\n    },\n    meta: {\n      id: '1487805099387 100:uImkcF4z 0',\n      reasons: ['lastValue', 'debug'],\n      server: 'server:H1f8LAyzl',\n      subprotocol: 1,\n      time: 1487805099387\n    }\n  })\n})\n\nit('reports add and clean', () => {\n  check('addClean', {\n    action: {\n      data: {\n        array: [1, [2], { a: '1', b: { c: 2 }, d: [], e: null }, null],\n        name: 'John',\n        role: null\n      },\n      id: 100,\n      type: 'CHANGE_USER'\n    },\n    meta: {\n      id: '1487805099387 100:uImkcF4z 0',\n      reasons: ['lastValue', 'debug'],\n      server: 'server:H1f8LAyzl',\n      subprotocol: 1,\n      time: 1487805099387\n    }\n  })\n})\n\nit('throws on circuital reference', () => {\n  let a: { b: any } = { b: undefined }\n  let b: { a: any } = { a: undefined }\n  a.b = b\n  b.a = a\n  expect(() => {\n    check('add', {\n      action: { a, type: 'CHANGE_USER' },\n      meta: {\n        id: '1487805099387 100:uImkcF4z 0',\n        reasons: ['lastValue', 'debug'],\n        server: 'server:H1f8LAyzl',\n        subprotocol: 1,\n        time: 1487805099387\n      }\n    })\n  }).toThrow('Circular reference in action')\n})\n\nit('reports clean', () => {\n  check('clean', {\n    actionId: '1487805099387 100:uImkcF4z 0'\n  })\n})\n\nit('reports denied', () => {\n  check('denied', {\n    actionId: '1487805099387 100:uImkcF4z 0'\n  })\n})\n\nit('reports unknownType', () => {\n  check('unknownType', {\n    actionId: '1487805099387 100:vAApgNT9 0',\n    type: 'CHANGE_SER'\n  })\n})\n\nit('reports unknownType from server', () => {\n  check('unknownType', {\n    actionId: '1650269021700 server:FnXaqDxY 0',\n    type: 'CHANGE_SER'\n  })\n})\n\nit('reports wrongChannel', () => {\n  check('wrongChannel', {\n    actionId: '1650269045800 100:IsvVzqWx 0',\n    channel: 'ser/100'\n  })\n})\n\nit('reports wrongChannel without name', () => {\n  check('wrongChannel', {\n    actionId: '1650269056600 100:uImkcF4z 0',\n    channel: undefined\n  })\n})\n\nit('reports subscribed', () => {\n  check('subscribed', {\n    actionId: '1487805099387 100:uImkcF4z 0',\n    channel: 'user/100'\n  })\n})\n\nit('reports unsubscribed', () => {\n  check('unsubscribed', {\n    actionId: '1650271940900 100:uImkcF4z 0',\n    channel: 'user/100'\n  })\n})\n\nit('reports disconnect', () => {\n  check('disconnect', { nodeId: '100:uImkcF4z' })\n})\n\nit('reports disconnect from unauthenticated user', () => {\n  check('disconnect', { connectionId: '670' })\n})\n\nit('reports zombie', () => {\n  check('zombie', { nodeId: '100:uImkcF4z' })\n})\n\nit('reports destroy', () => {\n  check('destroy')\n})\n\nit('reports EACCES error', () => {\n  check('error', { err: { code: 'EACCES', port: 80 }, fatal: true })\n})\n\n// Old Node.js color formatter is different\ndescribe.runIf(\n  !process.version.startsWith('v20.') && !process.version.startsWith('v22.')\n)('stacktrace', () => {\n  it('reports EADDRINUSE error', () => {\n    check('error', {\n      err: { code: 'EADDRINUSE', port: 31337 },\n      fatal: true\n    })\n  })\n\n  it('reports Logux error', () => {\n    let err = {\n      logux: true,\n      message: 'Unknown option `suprotocol` in server constructor',\n      note:\n        'Maybe there is a mistake in option name or this version ' +\n        'of Logux Server doesn’t support this option'\n    }\n    check('error', { err, fatal: true })\n  })\n\n  it('reports sync error', () => {\n    let err = new LoguxError('unknown-message', 'bad', true)\n    check('error', { connectionId: '670', err })\n  })\n})\n\nit('reports error', () => {\n  check('error', {\n    err: createError('Error', 'Some mistake'),\n    fatal: true\n  })\n})\n\nit('reports error from action', () => {\n  check('error', {\n    actionId: '1487805099387 100:uImkcF4z 0',\n    err: createError('Error', 'Some mistake')\n  })\n})\n\nit('reports error with token', () => {\n  check('error', {\n    actionId: '1487805099387 100:uImkcF4z 0',\n    err: createError('Error', '{\"Authorization\":\"Bearer secret\"}')\n  })\n})\n\nit('reports error from client', () => {\n  let err = new LoguxError('timeout', 5000, true)\n  check('clientError', { connectionId: '670', err })\n})\n\nit('reports error from node', () => {\n  let err = new LoguxError('timeout', 5000, false)\n  check('clientError', { err, nodeId: '100:uImkcF4z' })\n})\n\nit('reports useless actions', () => {\n  check('useless', {\n    action: {\n      id: 100,\n      name: 'John',\n      type: 'ADD_USER'\n    },\n    meta: {\n      id: '1487805099387 100:uImkcF4z 0',\n      reasons: [],\n      server: 'server:H1f8LAyzl',\n      subprotocol: 1,\n      time: 1487805099387\n    }\n  })\n})\n\nit(\"reports actions with metadata containing 'clients' array\", () => {\n  check('add', {\n    action: {\n      id: 100,\n      name: 'John',\n      type: 'ADD_USER'\n    },\n    meta: {\n      clients: ['1:-lCr7e9s', '2:wv0r_O5C'],\n      id: '1487805099387 100:uImkcF4z 0',\n      reasons: [],\n      server: 'server:H1f8LAyzl',\n      subprotocol: 1,\n      time: 1487805099387\n    }\n  })\n})\n\nit('reports actions with excludeClients metadata', () => {\n  check('add', {\n    action: {\n      id: 100,\n      name: 'John',\n      type: 'ADD_USER'\n    },\n    meta: {\n      excludeClients: ['1:-lCr7e9s', '2:wv0r_O5C'],\n      id: '1487805099387 100:uImkcF4z 0',\n      reasons: [],\n      server: 'server:H1f8LAyzl',\n      subprotocol: 1,\n      time: 1487805099387\n    }\n  })\n})\n\nit('allows custom loggers', () => {\n  let text = new MemoryStream(false)\n  let jsonReporter = createReporter({\n    logger: {\n      info(details: object, msg: string) {\n        text.write(JSON.stringify({ details, msg }))\n      }\n    }\n  })\n  jsonReporter('connect', { connectionId: '670', ipAddress: '10.110.6.56' })\n  expect(clean(text.string)).toMatchSnapshot()\n})\n"
  },
  {
    "path": "filter-meta/index.d.ts",
    "content": "import type { ServerMeta } from '../base-server/index.js'\n\n/**\n * Remove all non-allowed keys from meta.\n *\n * @param meta Meta to remove keys.\n * @returns Meta with removed keys.\n */\nexport function filterMeta(meta: ServerMeta): ServerMeta\n"
  },
  {
    "path": "filter-meta/index.js",
    "content": "import { ALLOWED_META } from '../allowed-meta/index.js'\n\nexport function filterMeta(meta) {\n  let result = {}\n  for (let i of ALLOWED_META) {\n    if (typeof meta[i] !== 'undefined') result[i] = meta[i]\n  }\n  return result\n}\n"
  },
  {
    "path": "filter-meta/index.test.ts",
    "content": "import { expect, it } from 'vitest'\n\nimport { filterMeta, type ServerMeta } from '../index.js'\n\nit('filters meta', () => {\n  let meta1: ServerMeta = {\n    added: 0,\n    id: '1 test 0',\n    reasons: [],\n    server: '',\n    status: 'processed',\n    time: 0\n  }\n  expect(filterMeta(meta1)).toEqual({ id: '1 test 0', time: 0 })\n  let meta2: ServerMeta = {\n    added: 0,\n    id: '1 test 0',\n    reasons: [],\n    server: '',\n    subprotocol: 1,\n    time: 0\n  }\n  expect(filterMeta(meta2).subprotocol).toEqual(1)\n})\n"
  },
  {
    "path": "filtered-node/index.js",
    "content": "import { ServerNode } from '@logux/core'\n\nfunction has(array, item) {\n  return array && array.includes(item)\n}\n\nexport class FilteredNode extends ServerNode {\n  constructor(client, nodeId, log, connection, options) {\n    super(nodeId, log, connection, options)\n    this.client = client\n\n    // Remove add event listener\n    this.unbind[0]()\n    this.unbind.splice(0, 1)\n\n    delete this.received\n  }\n\n  syncFilter(action, meta) {\n    return (\n      (has(meta.clients, this.client.clientId) ||\n        has(meta.nodes, this.client.nodeId) ||\n        has(meta.users, this.client.userId)) &&\n      !has(meta.excludeClients, this.client.clientId)\n    )\n  }\n}\n"
  },
  {
    "path": "filtered-node/index.test.ts",
    "content": "import { ClientNode, type TestLog, TestPair, TestTime } from '@logux/core'\nimport { afterEach, expect, it } from 'vitest'\n\nimport { FilteredNode } from '../filtered-node/index.js'\n\ntype Test = {\n  client: ClientNode<object, TestLog>\n  server: FilteredNode\n}\n\nfunction createTest(): Test {\n  let time = new TestTime()\n  let log1 = time.nextLog()\n  let log2 = time.nextLog()\n\n  log1.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n  log2.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n\n  let data = { clientId: '1:a', nodeId: '1:a:b', userId: '1' }\n  let pair = new TestPair()\n  let client = new ClientNode('1:a:b', log1, pair.left)\n  let server = new FilteredNode(data, 'server', log2, pair.right)\n  return { client, server }\n}\n\nlet test: Test\nafterEach(() => {\n  test.client.destroy()\n  test.server.destroy()\n})\n\nit('does not sync actions on add', async () => {\n  test = createTest()\n  await test.client.connection.connect()\n  await test.client.waitFor('synchronized')\n  await test.server.log.add({ type: 'A' })\n  await test.server.waitFor('synchronized')\n  expect(test.client.log.actions()).toEqual([])\n})\n\nit('synchronizes only node-specific actions on connection', async () => {\n  test = createTest()\n  await test.server.log.add({ type: 'A' }, { nodes: ['1:A:B'] })\n  await test.server.log.add({ type: 'B' }, { nodes: ['1:a:b'] })\n  await test.server.log.add({ type: 'C' })\n  await test.client.connection.connect()\n\n  await test.server.waitFor('synchronized')\n\n  expect(test.client.log.actions()).toEqual([{ type: 'B' }])\n})\n\nit('synchronizes only client-specific actions on connection', async () => {\n  test = createTest()\n  await test.server.log.add({ type: 'A' }, { clients: ['1:A'] })\n  await test.server.log.add({ type: 'B' }, { clients: ['1:a'] })\n  await test.server.log.add({ type: 'C' })\n  await test.client.connection.connect()\n\n  await test.server.waitFor('synchronized')\n\n  expect(test.client.log.actions()).toEqual([{ type: 'B' }])\n})\n\nit('synchronizes only user-specific actions on connection', async () => {\n  test = createTest()\n  await test.server.log.add({ type: 'A' }, { users: ['2'] })\n  await test.server.log.add({ type: 'B' }, { users: ['1'] })\n  await test.server.log.add({ type: 'C' })\n  await test.client.connection.connect()\n\n  await test.server.waitFor('synchronized')\n\n  expect(test.client.log.actions()).toEqual([{ type: 'B' }])\n})\n\nit('still sends only new actions', async () => {\n  test = createTest()\n  await test.server.log.add({ type: 'A' }, { nodes: ['1:a:b'] })\n  await test.server.log.add({ type: 'B' }, { nodes: ['1:a:b'] })\n\n  test.client.lastReceived = 1\n  await test.client.connection.connect()\n\n  await test.server.waitFor('synchronized')\n\n  expect(test.client.log.actions()).toEqual([{ type: 'B' }])\n})\n"
  },
  {
    "path": "human-formatter/index.js",
    "content": "import os from 'node:os'\nimport { stripVTControlCharacters, styleText } from 'node:util'\n\nimport { mulberry32, onceXmur3 } from './utils.js'\n\nconst INDENT = '  '\nconst PADDING = '        '\nconst SEPARATOR = os.EOL + os.EOL\nconst NEXT_LINE = os.EOL === '\\n' ? '\\r\\v' : os.EOL\n\nconst PARAMS_BLACKLIST = {\n  component: true,\n  err: true,\n  hint: true,\n  hostname: true,\n  level: true,\n  listen: true,\n  msg: true,\n  name: true,\n  note: true,\n  pid: true,\n  server: true,\n  time: true,\n  v: true\n}\n\nconst LABELS = {\n  20: str => label(' DEBUG ', 'white', 'bgWhite', 'black', str),\n  30: str => label(' INFO ', 'green', 'bgGreen', 'black', str),\n  40: str => label(' WARN ', 'yellow', 'bgYellow', 'black', str),\n  50: str => label(' ERROR ', 'red', 'bgRed', 'white', str),\n  60: str => label(' FATAL ', 'red', 'bgRed', 'white', str)\n}\n\nconst COLORS = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan']\n\nfunction formatNow() {\n  let date = new Date()\n  let year = date.getFullYear()\n  let month = String(date.getMonth() + 1).padStart(2, '0')\n  let day = String(date.getDate()).padStart(2, '0')\n  let hour = String(date.getHours()).padStart(2, '0')\n  let minutes = String(date.getMinutes()).padStart(2, '0')\n  let seconds = String(date.getSeconds()).padStart(2, '0')\n  return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}`\n}\n\nfunction rightPag(str, length) {\n  let add = length - stripVTControlCharacters(str).length\n  for (let i = 0; i < add; i++) str += ' '\n  return str\n}\n\nfunction label(type, color, labelBg, labelText, message) {\n  let pagged = rightPag(styleText(labelBg, styleText(labelText, type)), 8)\n  let time = styleText('dim', `at ${formatNow()}`)\n  let highlighted = message.replace(/`([^`]+)`/g, styleText('yellow', '$1'))\n  return `${pagged}${styleText('bold', styleText(color, highlighted))} ${time}`\n}\n\nfunction formatName(key) {\n  return key\n    .replace(/[A-Z]/g, char => ` ${char.toLowerCase()}`)\n    .split(' ')\n    .map(word => (word === 'ip' || word === 'id' ? word.toUpperCase() : word))\n    .join(' ')\n    .replace(/^\\w/, char => char.toUpperCase())\n}\n\nfunction shuffledColors(str) {\n  let index = -1\n  let result = Array.from(COLORS)\n  let lastIndex = result.length - 1\n  let seed = onceXmur3(str)\n  let randomFn = mulberry32(seed)\n\n  while (++index < COLORS.length) {\n    let randIndex = index + Math.floor(randomFn() * (lastIndex - index + 1))\n    let value = result[randIndex]\n\n    result[randIndex] = result[index]\n    result[index] = value\n  }\n  return result\n}\n\nfunction splitAndColorize(partLength, str) {\n  let strBuilder = []\n  let colors = shuffledColors(str)\n\n  for (\n    let start = 0, end = partLength, n = 0, color = colors[n];\n    start < str.length;\n    start += partLength,\n      end += partLength,\n      n = n + 1,\n      color = colors[n % colors.length]\n  ) {\n    let strToColorize = str.slice(start, end)\n    if (strToColorize.length === 1) {\n      color = colors[Math.abs(n - 1) % colors.length]\n    }\n    strBuilder.push(styleText(color, strToColorize))\n  }\n\n  return strBuilder.join('')\n}\n\nfunction formatNodeId(nodeId) {\n  let pos = nodeId.lastIndexOf(':')\n  if (pos === -1) {\n    return nodeId\n  } else {\n    let s = nodeId.split(':')\n    let id = styleText('bold', s[0])\n    let random = splitAndColorize(3, s[1])\n    return `${id}:${random}`\n  }\n}\n\nfunction formatValue(value) {\n  if (typeof value === 'string') {\n    return '\"' + styleText('bold', value) + '\"'\n  } else if (Array.isArray(value)) {\n    return formatArray(value)\n  } else if (typeof value === 'object' && value) {\n    return formatObject(value)\n  } else {\n    return styleText('bold', `${value}`)\n  }\n}\n\nfunction formatObject(obj) {\n  let items = Object.keys(obj).map(k => `${k}: ${formatValue(obj[k])}`)\n  return '{ ' + items.join(', ') + ' }'\n}\n\nfunction formatArray(array) {\n  let items = array.map(i => formatValue(i))\n  return '[' + items.join(', ') + ']'\n}\n\nfunction formatActionId(id) {\n  let p = id.split(' ')\n  if (p.length === 1) {\n    return p\n  }\n  return (\n    `${styleText('bold', splitAndColorize(4, p[0]))} ` +\n    `${formatNodeId(p[1])} ${styleText('bold', p[2])}`\n  )\n}\n\nfunction formatParams(params, parent) {\n  let maxName = params.reduce((max, param) => {\n    let name = param[0]\n    return name.length > max ? name.length : max\n  }, 0)\n\n  return params\n    .map(param => {\n      let name = param[0]\n      let value = param[1]\n\n      let start = PADDING + rightPag(`${name}: `, maxName + 2)\n\n      if (name === 'Node ID' || (parent === 'Meta' && name === 'server')) {\n        return start + formatNodeId(value)\n      } else if (\n        parent === 'Meta' &&\n        (name === 'clients' || name === 'excludeClients')\n      ) {\n        return `${start}[${value.map(v => `\"${formatNodeId(v)}\"`).join()}]`\n      } else if (name === 'Action ID' || (parent === 'Meta' && name === 'id')) {\n        return start + formatActionId(value)\n      } else if (Array.isArray(value)) {\n        return start + formatArray(value)\n      } else if (typeof value === 'object' && value) {\n        let nested = Object.keys(value).map(key => [key, value[key]])\n        return (\n          start +\n          NEXT_LINE +\n          INDENT +\n          formatParams(nested, name)\n            .split(NEXT_LINE)\n            .join(NEXT_LINE + INDENT)\n        )\n      } else if (typeof value === 'string' && parent) {\n        return start + '\"' + styleText('bold', value) + '\"'\n      } else {\n        return start + styleText('bold', `${value}`)\n      }\n    })\n    .join(NEXT_LINE)\n}\n\nfunction splitByLength(string, max) {\n  let words = string.split(' ')\n  let lines = ['']\n  for (let word of words) {\n    let last = lines[lines.length - 1]\n    if (last.length + word.length > max) {\n      lines.push(`${word} `)\n    } else {\n      lines[lines.length - 1] = `${last}${word} `\n    }\n  }\n  return lines.map(i => i.trim())\n}\n\nfunction prettyStackTrace(stack, basepath) {\n  return stack\n    .split('\\n')\n    .slice(1)\n    .map(line => {\n      let match = line.match(/\\s+at ([^(]+) \\(([^)]+)\\)/)\n      let isSystem = !match || !match[2].startsWith(basepath)\n      if (isSystem) {\n        return styleText('gray', line.replace(/^\\s*/, PADDING))\n      } else {\n        let func = match[1]\n        let relative = match[2].slice(basepath.length)\n        let converted = `${PADDING}at ${func} (./${relative})`\n        let isDependency = match[2].includes('node_modules')\n        return isDependency\n          ? styleText('gray', converted)\n          : styleText('red', converted)\n      }\n    })\n    .join(NEXT_LINE)\n}\n\nexport default function humanFormatter(options) {\n  let basepath = options.basepath\n\n  return function format(record) {\n    let message = [LABELS[record.level](record.msg)]\n    let params = Object.keys(record)\n      .filter(key => !PARAMS_BLACKLIST[key])\n      .map(key => [formatName(key), record[key]])\n\n    if (record.loguxServer) {\n      params.unshift(['PID', record.pid])\n      if (record.server) {\n        params.push(['Listen', 'Custom HTTP server'])\n      } else {\n        params.push(['Listen', record.listen])\n      }\n    }\n\n    if (record.err && record.err.stack) {\n      message.push(prettyStackTrace(record.err.stack, basepath))\n    }\n\n    message.push(formatParams(params))\n\n    if (record.note) {\n      let note = record.note\n      if (typeof note === 'string') {\n        note = note.replace(/`([^`]+)`/g, styleText('bold', '$1'))\n        note = [].concat(\n          ...note\n            .split('\\n')\n            .map(row => splitByLength(row, 80 - PADDING.length))\n        )\n      }\n      message.push(\n        note.map(i => PADDING + styleText('gray', i)).join(NEXT_LINE)\n      )\n    }\n\n    return message.filter(i => i !== '').join(NEXT_LINE) + SEPARATOR\n  }\n}\n"
  },
  {
    "path": "human-formatter/utils.js",
    "content": "export function onceXmur3(str) {\n  let h = 1779033703 ^ str.length\n  for (let i = 0; i < str.length; i++) {\n    h = Math.imul(h ^ str.charCodeAt(i), 3432918353)\n    h = (h << 13) | (h >>> 19)\n  }\n  h = Math.imul(h ^ (h >>> 16), 2246822507)\n  h = Math.imul(h ^ (h >>> 13), 3266489909)\n  return (h ^ (h >>> 16)) >>> 0\n}\n\nexport function mulberry32(a) {\n  return function () {\n    a |= 0\n    a = (a + 0x6d2b79f5) | 0\n    let t = Math.imul(a ^ (a >>> 15), 1 | a)\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n  }\n}\n"
  },
  {
    "path": "index.d.ts",
    "content": "export {\n  addSyncMap,\n  addSyncMapFilter,\n  ChangedAt,\n  NoConflictResolution,\n  SyncMapData,\n  WithoutTime,\n  WithTime\n} from './add-sync-map/index.js'\nexport { ALLOWED_META } from './allowed-meta/index.js'\nexport {\n  BaseServer,\n  BaseServerOptions,\n  Logger,\n  SendBackActions,\n  ServerMeta,\n  wasNot403\n} from './base-server/index.js'\nexport { ChannelContext, Context } from './context/index.js'\nexport { filterMeta } from './filter-meta/index.js'\nexport {\n  del,\n  get,\n  patch,\n  post,\n  put,\n  request,\n  ResponseError\n} from './request/index.js'\nexport { ServerClient } from './server-client/index.js'\nexport { Server, ServerOptions } from './server/index.js'\nexport { LoguxActionError, TestClient } from './test-client/index.js'\nexport { TestServer, TestServerOptions } from './test-server/index.js'\n\nexport { Action } from '@logux/core'\n"
  },
  {
    "path": "index.js",
    "content": "export {\n  addSyncMap,\n  addSyncMapFilter,\n  ChangedAt,\n  NoConflictResolution\n} from './add-sync-map/index.js'\nexport { ALLOWED_META } from './allowed-meta/index.js'\nexport { BaseServer, wasNot403 } from './base-server/index.js'\nexport { Context } from './context/index.js'\nexport { filterMeta } from './filter-meta/index.js'\nexport { ResponseError } from './request/index.js'\nexport { Server } from './server/index.js'\nexport { TestClient } from './test-client/index.js'\nexport { TestServer } from './test-server/index.js'\n"
  },
  {
    "path": "options-loader/__snapshots__/index.test.js.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`loadOptions > returns help 1`] = `\n\"Start Logux Server\n\n\u001b[1mOptions:\u001b[22m\n\u001b[33m--port  \u001b[39m\u001b[36m  \u001b[39mport\n\n\u001b[1mExamples:\u001b[22m\ntest.js\"\n`;\n\nexports[`loadOptions > throws on missing values 1`] = `\n[Error: Failed to parse \u001b[1mport\u001b[22m argument value. \nExpected \u001b[32mstring\u001b[39m, got \u001b[31mtrue\u001b[39m]\n`;\n\nexports[`loadOptions > throws on unknown args 1`] = `[Error: Unknown argument: --unknown]`;\n\nexports[`loadOptions > throws on unparsed args 1`] = `\n[Error: Failed to parse \u001b[1mport\u001b[22m argument value. \nExpected \u001b[32mnumber\u001b[39m, got \u001b[31mW_W\u001b[39m]\n`;\n\nexports[`parsers > number > should return error on invalid values 1`] = `\"Expected \u001b[32mnumber\u001b[39m, got \u001b[31mnot a number\u001b[39m\"`;\n\nexports[`parsers > oneOf > should return error on invalid values 1`] = `\"Expected \u001b[32mone of [\"1\",\"2\"]\u001b[39m, got \u001b[31m3\u001b[39m\"`;\n"
  },
  {
    "path": "options-loader/index.js",
    "content": "import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { styleText } from 'node:util'\n\nexport function loadOptions(spec, process, env) {\n  let rawCliArgs = gatherCliArgs(process.argv)\n  if (rawCliArgs['--help']) {\n    return [composeHelp(spec, process.argv), null]\n  }\n\n  let namesMap = {}\n  for (let key in spec.options) {\n    let option = spec.options[key]\n    namesMap[composeCliFullName(key)] = key\n    namesMap[composeEnvName(spec.envPrefix, key)] = key\n    if (option.alias) {\n      namesMap[composeCliAliasName(option.alias)] = key\n    }\n  }\n\n  let cliArgs = parseValues(spec, mapArgs(rawCliArgs, namesMap))\n  let dotenvArgs = loadEnv(env)\n  if (dotenvArgs) {\n    dotenvArgs = Object.fromEntries(\n      Object.entries(dotenvArgs).filter(([key]) =>\n        key.startsWith(spec.envPrefix)\n      )\n    )\n  }\n  let envArgs = Object.fromEntries(\n    Object.entries(process.env).filter(([key]) =>\n      key.startsWith(spec.envPrefix)\n    )\n  )\n  envArgs = parseValues(spec, mapArgs({ ...envArgs, ...dotenvArgs }, namesMap))\n  return [null, { ...envArgs, ...cliArgs }]\n}\n\nfunction gatherCliArgs(argv) {\n  let args = {}\n  let key = null\n  let value = []\n  for (let it of argv) {\n    if (it.startsWith('-')) {\n      if (key) {\n        args[key] = value\n        value = []\n      }\n      key = it\n    } else if (key) {\n      value = [...value, it]\n    }\n  }\n  if (key) {\n    args[key] = value\n  }\n  for (let k in args) {\n    if (args[k].length === 0) {\n      args[k] = true\n    } else if (args[k].length === 1) {\n      args[k] = args[k][0]\n    }\n  }\n  return args\n}\n\nfunction parseValues(spec, args) {\n  let parsed = {}\n  for (let key of Object.keys(args)) {\n    let parse = spec.options[key].parse || string\n    let parsingResult = parse(args[key])\n    if (parsingResult[0] === null) {\n      parsed[key] = parsingResult[1]\n    } else {\n      throw Error(\n        `Failed to parse ${styleText('bold', key)} argument value. \\n` +\n          parsingResult[0]\n      )\n    }\n  }\n  return parsed\n}\n\nfunction loadEnv(file) {\n  if (!file || !existsSync(file)) {\n    file = join(process.cwd(), '.env')\n    if (!existsSync(file)) return undefined\n  }\n  let result = {}\n  let lines\n  try {\n    lines = readFileSync(file, 'utf8').split('\\n')\n  } catch {\n    /* c8 ignore next 2 */\n    return undefined\n  }\n  for (let line of lines) {\n    if (line.trim().startsWith('#') || !line.includes('=')) {\n      continue\n    }\n    let [key, ...valueParts] = line.split('=')\n    result[key] = valueParts\n      .join('=')\n      .trim()\n      .replace(/(^\"|\"$|^'|'$)/g, '')\n  }\n  return result\n}\n\nfunction mapArgs(parsedCliArgs, argsSpec) {\n  return Object.fromEntries(\n    Object.entries(parsedCliArgs).map(([name, value]) => {\n      if (!argsSpec[name]) {\n        throw new Error(`Unknown argument: ${name}`)\n      }\n      return [argsSpec[name], value]\n    })\n  )\n}\n\nfunction composeHelp(spec, argv) {\n  let options = Object.entries(spec.options).map(([name, option]) => ({\n    alias: option.alias ? composeCliAliasName(option.alias) : '',\n    description: option.description,\n    env: spec.envPrefix ? composeEnvName(spec.envPrefix, name) : '',\n    full: composeCliFullName(name)\n  }))\n  let nameColumnLength = Math.max(...options.map(it => it.full.length))\n  let envColumnLength = Math.max(...options.map(it => it.env.length))\n\n  let composeName = name =>\n    styleText('yellow', name.padEnd(nameColumnLength + 2))\n  let composeEnv = env => styleText('cyan', env.padEnd(envColumnLength + 2))\n  let composeOptionHelp = option => {\n    return (\n      composeName(option.full) + composeEnv(option.env) + option.description\n    )\n  }\n\n  let examples = []\n  if (spec.examples) {\n    let pathParts = argv[1].split('/')\n    let lastPart = pathParts[pathParts.length - 1]\n    examples = [\n      '',\n      styleText('bold', 'Examples:'),\n      ...spec.examples.map(i => i.replace('$0', lastPart))\n    ]\n  }\n\n  return [\n    'Start Logux Server',\n    '',\n    styleText('bold', 'Options:'),\n    ...options.map(option => composeOptionHelp(option)),\n    ...examples\n  ].join('\\n')\n}\n\nfunction composeEnvName(prefix, name) {\n  return `${prefix}_${name.replace(\n    /[A-Z]/g,\n    match => '_' + match\n  )}`.toUpperCase()\n}\n\nfunction composeCliFullName(name) {\n  return `--${toKebabCase(name)}`\n}\n\nfunction composeCliAliasName(name) {\n  return `-${name}`\n}\n\nfunction toKebabCase(word) {\n  return word.replace(/[A-Z]/, match => `-${match.toLowerCase()}`)\n}\n\nexport function oneOf(options, rawValue) {\n  if (!options.includes(rawValue)) {\n    let opt = JSON.stringify(options)\n    return [\n      `Expected ${styleText('green', 'one of ' + opt)}, ` +\n        `got ${styleText('red', `${rawValue}`)}`,\n      null\n    ]\n  } else {\n    return [null, rawValue]\n  }\n}\n\nexport function number(rawValue) {\n  let parsed = Number.parseInt(rawValue, 10)\n  if (Number.isNaN(parsed)) {\n    return [\n      `Expected ${styleText('green', 'number')}, ` +\n        `got ${styleText('red', `${rawValue}`)}`,\n      null\n    ]\n  } else {\n    return [null, parsed]\n  }\n}\n\nexport function string(rawValue) {\n  if (typeof rawValue !== 'string') {\n    return [\n      `Expected ${styleText('green', 'string')}, ` +\n        `got ${styleText('red', `${rawValue}`)}`,\n      null\n    ]\n  } else {\n    return [null, rawValue]\n  }\n}\n"
  },
  {
    "path": "options-loader/index.test.js",
    "content": "import '../test/force-colors.js'\n\nimport { resolve } from 'node:path'\nimport { describe, expect, it } from 'vitest'\n\nimport { loadOptions, number, oneOf } from './index.js'\n\nfunction fakeProcess(argv, env = {}) {\n  return { argv, env }\n}\n\ndescribe('loadOptions', () => {\n  it('returns help', () => {\n    let [help, options] = loadOptions(\n      {\n        examples: ['$0'],\n        options: {\n          port: {\n            description: 'port',\n            parse: number\n          }\n        }\n      },\n      fakeProcess(['node', 'test/test.js', '--help'])\n    )\n\n    expect(help).toMatchSnapshot()\n    expect(options).toBeNull()\n  })\n\n  it('uses CLI args for options', () => {\n    let [, options] = loadOptions(\n      {\n        options: {\n          port: {\n            description: 'port',\n            parse: number\n          }\n        }\n      },\n      fakeProcess(['', '--port', '1337'])\n    )\n    expect(options.port).toEqual(1337)\n  })\n\n  it('uses env for options', () => {\n    let [, options] = loadOptions(\n      {\n        envPrefix: 'LOGUX',\n        options: {\n          port: {\n            description: 'port',\n            parse: number\n          }\n        }\n      },\n      fakeProcess([], {\n        LOGUX_PORT: '31337'\n      }),\n      {}\n    )\n\n    expect(options.port).toEqual(31337)\n  })\n\n  it('uses dotenv file for options', () => {\n    let [, options] = loadOptions(\n      {\n        envPrefix: 'LOGUX',\n        options: {\n          port: {\n            description: 'port',\n            parse: number\n          }\n        }\n      },\n      fakeProcess([], {}),\n      resolve(process.cwd(), 'options-loader/test.env')\n    )\n\n    expect(options.port).toEqual(31337)\n  })\n\n  it('composes correct env and CLI names for argument with complex name', () => {\n    let [, options] = loadOptions(\n      {\n        envPrefix: 'LOGUX',\n        options: {\n          somePort: {\n            description: 'port',\n            parse: number\n          }\n        }\n      },\n      fakeProcess(['--some-port', '1'], {\n        LOGUX_SOME_PORT: '1'\n      }),\n      {}\n    )\n\n    expect(options.somePort).toEqual(1)\n  })\n\n  it('uses combined options', () => {\n    let [, options] = loadOptions(\n      {\n        envPrefix: 'LOGUX',\n        options: {\n          cert: {\n            description: 'cert'\n          },\n          key: {\n            description: 'port'\n          }\n        }\n      },\n      fakeProcess(['', '--key', './key.pem'], { LOGUX_CERT: './cert.pem' })\n    )\n\n    expect(options.cert).toEqual('./cert.pem')\n    expect(options.key).toEqual('./key.pem')\n  })\n\n  it('uses arg and env in given priority', () => {\n    let optionsSpec = {\n      envPrefix: 'LOGUX',\n      options: {\n        cert: {\n          description: 'cert'\n        },\n        key: {\n          description: 'key'\n        },\n        port: {\n          description: 'port',\n          parse: number\n        }\n      }\n    }\n\n    let [, options1] = loadOptions(\n      optionsSpec,\n      fakeProcess(['', '--port', '3'], { LOGUX_PORT: '2' }),\n      undefined\n    )\n    let [, options2] = loadOptions(\n      optionsSpec,\n      fakeProcess([], { LOGUX_PORT: '2' }),\n      undefined\n    )\n\n    expect(options1.port).toEqual(3)\n    expect(options2.port).toEqual(2)\n  })\n\n  it('parses aliases', () => {\n    let [, options] = loadOptions(\n      {\n        options: {\n          port: {\n            alias: 'p',\n            description: 'port'\n          }\n        }\n      },\n      fakeProcess(['', '-p', '1'])\n    )\n    expect(options.port).toEqual('1')\n  })\n\n  it('parses multiple args', () => {\n    let [, options] = loadOptions(\n      {\n        options: {\n          key: {\n            description: 'key'\n          },\n          port: {\n            alias: 'p',\n            description: 'port'\n          }\n        }\n      },\n      fakeProcess(['', '-p', '1', '--key', '1'])\n    )\n    expect(options.port).toEqual('1')\n    expect(options.key).toEqual('1')\n  })\n\n  it('throws on missing values', () => {\n    expect(() =>\n      loadOptions(\n        {\n          options: {\n            port: {\n              description: 'port'\n            }\n          }\n        },\n        fakeProcess(['', '--port'])\n      )\n    ).toThrowErrorMatchingSnapshot()\n  })\n\n  it('throws on unknown args', () => {\n    expect(() =>\n      loadOptions(\n        {\n          options: {\n            port: {\n              description: 'port'\n            }\n          }\n        },\n        fakeProcess(['', '--unknown', '1'])\n      )\n    ).toThrowErrorMatchingSnapshot()\n  })\n\n  it('throws on unparsed args', () => {\n    expect(() =>\n      loadOptions(\n        {\n          options: {\n            port: {\n              description: 'port',\n              parse: number\n            }\n          }\n        },\n        fakeProcess(['', '--port', 'W_W'])\n      )\n    ).toThrowErrorMatchingSnapshot()\n  })\n})\n\ndescribe('parsers', () => {\n  describe('oneOf', () => {\n    it('should return error on invalid values', () => {\n      let result = oneOf(['1', '2'], '3')\n      expect(result[0]).toMatchSnapshot()\n    })\n    it('should return null on correct values', () => {\n      expect(oneOf(['1', '2'], '1')).toEqual([null, '1'])\n    })\n  })\n\n  describe('number', () => {\n    it('should return error on invalid values', () => {\n      let result = number('not a number')\n      expect(result[0]).toMatchSnapshot()\n    })\n    it('should return null on correct values', () => {\n      expect(number('1')).toEqual([null, 1])\n      expect(number('1why parseInt is so permissive')).toEqual([null, 1])\n    })\n  })\n})\n"
  },
  {
    "path": "options-loader/test.env",
    "content": "# Comment\n\nDATABASE_URL=postgresql://localhost/logux\nLOGUX_PORT=31337\n"
  },
  {
    "path": "oxfmt.config.ts",
    "content": "import loguxOxfmtConfig from '@logux/oxc-configs/fmt'\n\nexport default loguxOxfmtConfig\n"
  },
  {
    "path": "oxlint.config.ts",
    "content": "import loguxOxlintConfig from '@logux/oxc-configs/lint'\nimport { defineConfig } from 'oxlint'\n\nexport default defineConfig({\n  extends: [loguxOxlintConfig],\n  ignorePatterns: ['**/errors.ts'],\n  rules: {\n    'typescript/no-unnecessary-type-parameters': 'off',\n    'typescript/no-unnecessary-type-arguments': 'off',\n    'unicorn/prefer-add-event-listener': 'off',\n    'node/handle-callback-err': 'off',\n    'import/no-named-as-default': 'off'\n  },\n  overrides: [\n    {\n      files: ['test/**/*', '*/*.test.ts'],\n      rules: {\n        'typescript/no-unsafe-function-type': 'off',\n        'typescript/require-await': 'off',\n        'no-console': 'off'\n      }\n    }\n  ]\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@logux/server\",\n  \"version\": \"0.14.0\",\n  \"description\": \"Build own Logux server\",\n  \"keywords\": [\n    \"collaborative\",\n    \"crdt\",\n    \"distributed systems\",\n    \"event sourcing\",\n    \"framework\",\n    \"logux\",\n    \"proxy\",\n    \"server\",\n    \"websocket\"\n  ],\n  \"homepage\": \"https://logux.org/\",\n  \"license\": \"MIT\",\n  \"author\": \"Andrey Sitnik <andrey@sitnik.es>\",\n  \"repository\": \"logux/server\",\n  \"type\": \"module\",\n  \"types\": \"./index.d.ts\",\n  \"exports\": {\n    \".\": \"./index.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"test:lint\": \"oxlint\",\n    \"test:types\": \"check-dts\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test\": \"pnpm run /^test:/\"\n  },\n  \"dependencies\": {\n    \"@logux/actions\": \"^0.5.0\",\n    \"@logux/core\": \"^0.10.0\",\n    \"cookie\": \"^1.1.1\",\n    \"fastq\": \"^1.20.1\",\n    \"nanoevents\": \"^9.1.0\",\n    \"nanoid\": \"^5.1.9\",\n    \"tinyglobby\": \"^0.2.16\",\n    \"url-pattern\": \"^1.0.3\",\n    \"ws\": \"^8.20.0\"\n  },\n  \"devDependencies\": {\n    \"@logux/oxc-configs\": \"^0.3.4\",\n    \"@types/cross-spawn\": \"^6.0.6\",\n    \"@types/node\": \"^25.6.0\",\n    \"@types/ws\": \"^8.18.1\",\n    \"@vitest/coverage-v8\": \"^4.1.4\",\n    \"actions-up\": \"^1.14.0\",\n    \"check-dts\": \"^1.0.0\",\n    \"clean-publish\": \"^6.0.5\",\n    \"cross-spawn\": \"^7.0.6\",\n    \"eslint-plugin-prefer-let\": \"^4.2.2\",\n    \"nanospy\": \"^1.0.0\",\n    \"oxlint\": \"^1.60.0\",\n    \"oxlint-tsgolint\": \"^0.21.1\",\n    \"print-snapshots\": \"^0.4.2\",\n    \"typescript\": \"^6.0.2\",\n    \"vite\": \"^8.0.8\",\n    \"vitest\": \"^4.1.4\"\n  },\n  \"engines\": {\n    \"node\": \"^22.0.0 || >=24.0.0\"\n  }\n}\n"
  },
  {
    "path": "request/index.d.ts",
    "content": "/**\n * Throwing this error in `accessAndProcess` or `accessAndLoad`\n * will deny the action.\n */\nexport class ResponseError extends Error {\n  name: 'ResponseError'\n  statusCode: number\n\n  constructor(statusCode: number, url: string)\n}\n"
  },
  {
    "path": "request/index.js",
    "content": "export class ResponseError extends Error {\n  constructor(statusCode, url) {\n    super(`${statusCode} response on ${url}`)\n    this.name = 'ResponseError'\n    this.statusCode = statusCode\n  }\n}\n"
  },
  {
    "path": "server/__snapshots__/index.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`destroys everything on exit 1`] = `\n\" INFO   Logux server is listening at 1970-01-01 00:00:00\n        PID:             21384\n        Environment:     test\n        Logux server:    0.0.0\n        Min subprotocol: 1\n        Node ID:         server:FnXaqDxY\n        Subprotocol:     1\n        Health check:    http://127.0.0.1:2000/health\n        Listen:          ws://127.0.0.1:2000/\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n INFO   Custom destroy task finished at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`disables colors for constructor errors 1`] = `\n\" FATAL  Missed minSubprotocol option in server constructor at 1970-01-01 00:00:00\n        Check server constructor and Logux Server documentation\n\n\"\n`;\n\nexports[`has custom logger 1`] = `\n\" INFO   Hi from custom logger at 1970-01-01 00:00:00\n        Field: 1\n\n DEBUG  Debug message at 1970-01-01 00:00:00\n\n INFO   Logux server is listening at 1970-01-01 00:00:00\n        PID:             21384\n        Environment:     test\n        Logux server:    0.0.0\n        Min subprotocol: 1\n        Node ID:         server:FnXaqDxY\n        Subprotocol:     1\n        Health check:    http://127.0.0.1:31337/health\n        Listen:          ws://127.0.0.1:31337/\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`shows help 1`] = `\n\"Start Logux Server\n\nOptions:\n--cert             LOGUX_CERT             Path to SSL certificate\n--host             LOGUX_HOST             Host to bind server\n--key              LOGUX_KEY              Path to SSL key\n--logger           LOGUX_LOGGER           Logger type\n--min-subprotocol  LOGUX_MIN_SUBPROTOCOL  Check supported client subprotocols\n--port             LOGUX_PORT             Port to bind server\n--redis            LOGUX_REDIS            Redis URL for Logux Server Pro scaling\n--subprotocol      LOGUX_SUBPROTOCOL      Server subprotocol\n\nExamples:\noptions.js --port 31337 --host 127.0.0.1\nLOGUX_PORT=1337 LOGUX_HOST=127.0.0.1 options.js\n\"\n`;\n\nexports[`shows help about missed option 1`] = `\n\" FATAL  Missed minSubprotocol option in server constructor at 1970-01-01 00:00:00\n        Check server constructor and Logux Server documentation\n\n\"\n`;\n\nexports[`shows help about port in use 1`] = `\n\" FATAL  Port 2001 already in use at 1970-01-01 00:00:00\n        Another Logux server or other app already running on this port.\n        Probably you haven’t stopped server from other project or previous\n        version of this server was not killed.\n        \n        $ su - root\n        # netstat -nlp | grep 2001\n        Proto   Local Address   State    PID/Program name\n        tcp     0.0.0.0:2001    LISTEN   777/node\n        # sudo kill -9 777\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`shows help about privileged port 1`] = `\n\" FATAL  You are not allowed to run server on port 1000 at 1970-01-01 00:00:00\n        Non-privileged users can't start a listening socket on ports below 1024.\n        Try to change user or take another port.\n        \n        $ su - <username>\n        $ npm start -p 1000\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`shows help about unknown option 1`] = `\n\" FATAL  Missed minSubprotocol option in server constructor at 1970-01-01 00:00:00\n        Check server constructor and Logux Server documentation\n\n\"\n`;\n\nexports[`shows uncatch errors 1`] = `\n\" FATAL  Test Error at 1970-01-01 00:00:00\n        fake stacktrace\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n INFO   Fatal event: Test Error at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`shows uncatch rejects 1`] = `\n\" FATAL  Test Error at 1970-01-01 00:00:00\n        fake stacktrace\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n INFO   Fatal event: Test Error at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`uses .env cwd 1`] = `\n\" INFO   Logux server is listening at 1970-01-01 00:00:00\n        PID:             21384\n        Environment:     test\n        Logux server:    0.0.0\n        Min subprotocol: 1\n        Node ID:         server:FnXaqDxY\n        Subprotocol:     1\n        Health check:    http://127.0.0.1:3334/health\n        Listen:          ws://127.0.0.1:3334/\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`uses .env from root 1`] = `\n\" INFO   Logux server is listening at 1970-01-01 00:00:00\n        PID:             21384\n        Environment:     test\n        Logux server:    0.0.0\n        Min subprotocol: 1\n        Node ID:         server:FnXaqDxY\n        Subprotocol:     1\n        Health check:    http://127.0.0.1:3334/health\n        Listen:          ws://127.0.0.1:3334/\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`uses autoload modules 1`] = `\n\"Root path module: 1\nChild path module: 1\n INFO   Logux server is listening at 1970-01-01 00:00:00\n        PID:             21384\n        Environment:     test\n        Logux server:    0.0.0\n        Min subprotocol: 1\n        Node ID:         server:FnXaqDxY\n        Subprotocol:     1\n        Health check:    http://127.0.0.1:31337/health\n        Listen:          ws://127.0.0.1:31337/\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`uses autoload wrong export 1`] = `\n\" INFO   Logux server is listening at 1970-01-01 00:00:00\n        PID:             21384\n        Environment:     test\n        Logux server:    0.0.0\n        Min subprotocol: 1\n        Node ID:         server:FnXaqDxY\n        Subprotocol:     1\n        Health check:    http://127.0.0.1:31337/health\n        Listen:          ws://127.0.0.1:31337/\n\n FATAL  Server module should has default export with function that accepts a server at 1970-01-01 00:00:00\n        error-modules/wrond-export/index.js default export is string\n\n INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n\nexports[`uses environment variables for config 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"environment\":\"test\",\"loguxServer\":\"0.0.0\",\"minSubprotocol\":1,\"nodeId\":\"server:FnXaqDxY\",\"subprotocol\":1,\"listen\":\"ws://127.0.0.1:31337/\",\"healthCheck\":\"http://127.0.0.1:31337/health\",\"msg\":\"Logux server is listening\"}\n{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"msg\":\"Shutting down Logux server\"}\n\"\n`;\n\nexports[`uses logger param 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"environment\":\"test\",\"loguxServer\":\"0.0.0\",\"minSubprotocol\":1,\"nodeId\":\"server:FnXaqDxY\",\"subprotocol\":1,\"listen\":\"ws://127.0.0.1:31337/\",\"healthCheck\":\"http://127.0.0.1:31337/health\",\"msg\":\"Logux server is listening\"}\n{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"msg\":\"Shutting down Logux server\"}\n\"\n`;\n\nexports[`uses logger param for constructor errors 1`] = `\n\"{\"level\":60,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"note\":\"Check server constructor and Logux Server documentation\",\"msg\":\"Missed \\`minSubprotocol\\` option in server constructor\"}\n\"\n`;\n\nexports[`writes JSON log 1`] = `\n\"{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"environment\":\"test\",\"loguxServer\":\"0.0.0\",\"minSubprotocol\":1,\"nodeId\":\"server:FnXaqDxY\",\"subprotocol\":1,\"listen\":\"ws://127.0.0.1:2000/\",\"healthCheck\":\"http://127.0.0.1:2000/health\",\"msg\":\"Logux server is listening\"}\n{\"level\":30,\"time\":\"1970-01-01T00:00:00.000Z\",\"pid\":21384,\"msg\":\"Shutting down Logux server\"}\n\"\n`;\n\nexports[`writes about unbind 1`] = `\n\" INFO   Shutting down Logux server at 1970-01-01 00:00:00\n\n\"\n`;\n"
  },
  {
    "path": "server/errors.ts",
    "content": "import { LoguxSubscribeAction, defineAction } from '@logux/actions'\n\nimport { Server, Action } from '../index.js'\n\nlet server = new Server<{ locale: string }>(\n  Server.loadOptions(process, {\n    minSubprotocol: 1,\n    subprotocol: 1,\n    root: ''\n  })\n)\n\nserver.auth(({ userId, token, headers }) => {\n  // THROWS Property 'lang' does not exist on type '{ locale: string; }'\n  console.log(headers.lang)\n  return token === userId\n})\n\nclass User {\n  id: string\n  name: string\n\n  constructor(id: string) {\n    this.id = id\n    this.name = 'name'\n  }\n\n  async save(): Promise<void> {}\n}\n\ntype UserRenameAction = Action & {\n  type: 'user/rename'\n  userId: string\n  name: string\n}\n\ntype UserSubscribeAction = LoguxSubscribeAction & {\n  fields: ('name' | 'email')[]\n}\n\ntype UserData = {\n  user: User\n}\n\ntype UserParams = {\n  id: string\n}\n\ntype BadParams = number\n\nserver.type<UserRenameAction, UserData>('user/rename', {\n  access(ctx, action) {\n    ctx.data.user = new User(action.userId)\n    return true\n  },\n\n  // THROWS is not assignable to type 'Resender\n  resend(_, action) {\n    return {\n      subscriptions: `user/${action.userId}`\n    }\n  },\n\n  async process(ctx, action) {\n    // THROWS Property 'lang' does not exist on type '{ locale: string; }'\n    console.log(ctx.headers.lang)\n    // THROWS 'newName' does not exist on type 'Readonly<UserRenameAction>'\n    ctx.data.user.name = action.newName\n    // THROWS Property 'admin' does not exist on type 'UserData'.\n    await ctx.data.admin.save()\n  }\n})\n\n// THROWS No overload matches this call.\nserver.type('user/changeId', {\n  async process(_, action) {\n    let user = new User(action.userId)\n    user.id = action.newId\n    await user.save()\n  }\n})\n\n// THROWS \"bad\"' is not assignable to parameter of type 'RegExp | \"user/rename\"'\nserver.type<UserRenameAction>('bad', {\n  access() {\n    return true\n  }\n})\n\nserver.channel<UserParams, UserData, UserSubscribeAction>('user/:id', {\n  access() {\n    return true\n  },\n  // THROWS ...>' is not assignable to type 'FilterCreator\n  async filter(_, action) {\n    if (action.fields) {\n      return (_: any, otherAction: Action) => {\n        return (\n          action.fields.includes('name') && otherAction.type === 'user/rename'\n        )\n      }\n    } else {\n      return undefined\n    }\n  },\n  async load(ctx) {\n    // THROWS is not assignable to parameter of type 'AnyAction'\n    await ctx.sendBack({\n      userId: ctx.data.user.id,\n      name: ctx.data.user.name\n    })\n  }\n})\n\nserver.channel(/admin:\\d/, {\n  access(ctx, action, meta) {\n    console.log(meta.id, action.since)\n    // THROWS Property 'id' does not exist on type 'string[]'.\n    return ctx.params.id === ctx.userId\n  }\n})\n\n// THROWS Type 'number' does not satisfy the constraint 'string[]'.\nserver.channel<BadParams>('posts', {\n  access() {\n    return true\n  }\n})\n\nlet addUser = defineAction<{ type: 'user/remove'; userId: string }>(\n  'user/remove'\n)\n\nserver.type(addUser, {\n  access(ctx, action) {\n    // THROWS Property 'id' does not exist on type 'Readonly<{ type: \"user/remove\";\n    return action.id === ctx.userId\n  }\n})\n"
  },
  {
    "path": "server/index.d.ts",
    "content": "import {\n  BaseServer,\n  type BaseServerOptions,\n  type Logger\n} from '../base-server/index.js'\n\nexport interface LogStream {\n  /**\n   * Used to synchronously write log messages on application failure.\n   */\n  flushSync?(): void\n\n  write(str: string): void\n}\n\nexport interface LoggerOptions {\n  /**\n   * Use color for human output.\n   */\n  color?: boolean\n\n  /**\n   * Stream to be used by logger to write log.\n   */\n  stream?: LogStream\n\n  /**\n   * Logger message format.\n   */\n  type?: 'human' | 'json'\n}\n\nexport interface ServerOptions extends BaseServerOptions {\n  /**\n   * Logger with custom settings.\n   *\n   * You can either configure built-in logger or provide your own.\n   */\n  logger?: Logger | LoggerOptions\n}\n\n/**\n * End-user API to create Logux server.\n *\n * ```js\n * import { Server } from '@logux/server'\n *\n * const env = process.env.NODE_ENV || 'development'\n * const envOptions = {}\n * if (env === 'production') {\n *   envOptions.cert = 'cert.pem'\n *   envOptions.key = 'key.pem'\n * }\n *\n * const server = new Server(Object.assign({\n *   subprotocol: 1,\n *   minSubprotocol: 1,\n *   root: import.meta.dirname\n * }, envOptions))\n *\n * server.listen()\n * ```\n */\nexport class Server<\n  Headers extends object = unknown\n> extends BaseServer<Headers> {\n  /**\n   * Server options.\n   *\n   * ```js\n   * console.log('Server options', server.options.subprotocol)\n   * ```\n   */\n  options: ServerOptions\n\n  /**\n   * @param opts Server options.\n   */\n  constructor(opts: ServerOptions)\n\n  /**\n   * Load options from command-line arguments and/or environment.\n   *\n   * ```js\n   * const server = new Server(Server.loadOptions(process, {\n   *   minSubprotocol: 1,\n   *   subprotocol: 1,\n   *   root: import.meta.dirname,\n   *   port: 31337\n   * }))\n   * ```\n   *\n   * @param process Current process object.\n   * @param defaults Default server options. Arguments and environment\n   *                 variables will override them.\n   * @returns Parsed options object.\n   */\n  static loadOptions(\n    process: NodeJS.Process,\n    defaults: ServerOptions\n  ): ServerOptions\n\n  /**\n   * Load module creators and apply to the server. By default, it will load\n   * files from `modules/*`.\n   *\n   * ```js\n   * await server.autoloadModules()\n   * ```\n   *\n   * @param files Pattern for module files.\n   */\n  autoloadModules(files?: string | string[]): Promise<void>\n}\n"
  },
  {
    "path": "server/index.js",
    "content": "import { join, relative } from 'node:path'\nimport { styleText } from 'node:util'\nimport { glob } from 'tinyglobby'\n\nimport { BaseServer } from '../base-server/index.js'\nimport { createReporter } from '../create-reporter/index.js'\nimport { loadOptions, number, oneOf } from '../options-loader/index.js'\n\nlet cliOptionsSpec = {\n  envPrefix: 'LOGUX',\n  examples: [\n    '$0 --port 31337 --host 127.0.0.1',\n    'LOGUX_PORT=1337 LOGUX_HOST=127.0.0.1 $0'\n  ],\n  options: {\n    cert: {\n      description: 'Path to SSL certificate'\n    },\n    host: {\n      alias: 'h',\n      description: 'Host to bind server'\n    },\n    key: {\n      description: 'Path to SSL key'\n    },\n    logger: {\n      alias: 'l',\n      description: 'Logger type',\n      parse: value => oneOf(['human', 'json'], value)\n    },\n    minSubprotocol: {\n      description: 'Check supported client subprotocols'\n    },\n    port: {\n      alias: 'p',\n      description: 'Port to bind server',\n      parse: number\n    },\n    redis: {\n      description: 'Redis URL for Logux Server Pro scaling'\n    },\n    subprotocol: {\n      description: 'Server subprotocol'\n    }\n  }\n}\n\nexport class Server extends BaseServer {\n  constructor(opts = {}) {\n    if (!opts.logger) {\n      opts.logger = 'human'\n    }\n\n    let reporter = createReporter(opts)\n\n    let initialized = false\n    let onError = err => {\n      if (initialized) {\n        this.emitter.emit('fatal', err)\n      } else {\n        reporter('error', { err, fatal: true })\n        process.exit(1)\n      }\n    }\n    process.on('uncaughtException', onError)\n    process.on('unhandledRejection', onError)\n\n    super(opts)\n\n    this.logger = reporter.logger\n    this.on('report', reporter)\n    this.on('fatal', async () => {\n      if (initialized) {\n        if (!this.destroying) {\n          await this.destroy()\n          process.exit(1)\n        }\n      } else {\n        process.exit(1)\n      }\n    })\n\n    initialized = true\n\n    let onExit = async () => {\n      await this.destroy()\n      process.exit(0)\n    }\n    process.on('SIGINT', onExit)\n\n    this.unbind.push(() => {\n      process.removeListener('SIGINT', onExit)\n    })\n  }\n\n  static loadOptions(process, defaults) {\n    let [help, options] = loadOptions(\n      cliOptionsSpec,\n      process,\n      defaults.root ? join(defaults.root, '.env') : undefined\n    )\n    if (help) {\n      process.stdout.write(help + '\\n')\n      return process.exit(0)\n    }\n    try {\n      return Object.assign(defaults, options)\n    } catch (e) {\n      process.stderr.write(\n        styleText(\n          'red',\n          styleText('bgRed', styleText('black', ' FATAL ')) + `${e.message}\\n`\n        )\n      )\n      return process.exit(1)\n    }\n  }\n\n  async autoloadModules(\n    files = ['modules/*/index.js', 'modules/*.js', '!**/*.{test,spec}.js']\n  ) {\n    if (!Array.isArray(files)) files = [files]\n    let matches = await glob(files, {\n      absolute: true,\n      cwd: this.options.root,\n      onlyFiles: true\n    })\n\n    await Promise.all(\n      matches.map(async file => {\n        let serverModule = (await import(file)).default\n        if (typeof serverModule === 'function') {\n          await serverModule(this)\n        } else {\n          let name = relative(this.options.root, file)\n          let error = new Error(\n            'Server module should has default export with function ' +\n              'that accepts a server'\n          )\n          error.logux = true\n          error.note = `${name} default export is ${typeof serverModule}`\n          throw error\n        }\n      })\n    )\n  }\n\n  async listen(...args) {\n    try {\n      return BaseServer.prototype.listen.apply(this, args)\n    } catch (err) {\n      this.emitter.emit('report', 'error', { err })\n      return process.exit(1)\n    }\n  }\n}\n"
  },
  {
    "path": "server/index.test.ts",
    "content": "import spawn from 'cross-spawn'\nimport type { ChildProcess, SpawnOptions } from 'node:child_process'\nimport { join } from 'node:path'\nimport { afterEach, expect, it } from 'vitest'\n\nimport { Server } from '../index.js'\n\nconst ROOT = join(import.meta.dirname, '..')\nconst DATE = /\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d/g\n\nlet started: ChildProcess | undefined\n\nfunction start(name: string, args?: string[]): Promise<void> {\n  return new Promise<void>(resolve => {\n    started = spawn(join(ROOT, 'test/servers/', name), args)\n    let running = false\n    function callback(): void {\n      if (!running) {\n        running = true\n        resolve()\n      }\n    }\n    started.stdout?.on('data', callback)\n    started.stderr?.on('data', callback)\n  })\n}\n\nfunction check(\n  name: string,\n  args?: string[],\n  opts?: SpawnOptions,\n  kill = false\n): Promise<[string, number]> {\n  return new Promise<[string, number]>(resolve => {\n    let out = ''\n    let server = spawn(join(ROOT, 'test/servers/', name), args, opts)\n    server.stdout?.on('data', chank => {\n      out += chank\n    })\n    server.stderr?.on('data', chank => {\n      out += chank\n    })\n    server.on('close', exit => {\n      let fixed = out\n        .replace(/[^\\n]+DeprecationWarning[^\\n]+\\n/gm, '')\n        .replace(DATE, '1970-01-01 00:00:00')\n        .replace(/\"time\":\"[^\"]+\"/g, '\"time\":\"1970-01-01T00:00:00.000Z\"')\n        .replace(/PID:(\\s+)\\d+/, 'PID:$121384')\n        .replace(/\"pid\":\\d+,/g, '\"pid\":21384,')\n        .replace(/Logux server:( +)\\d+.\\d+.\\d+/g, 'Logux server:$10.0.0')\n        .replace(/\"loguxServer\":\"\\d+.\\d+.\\d+\"/g, '\"loguxServer\":\"0.0.0\"')\n        .replace(/\"hostname\":\"[^\"]+\"/g, '\"hostname\":\"localhost\"')\n      fixed = fixed.replace(/\\r\\v/g, '\\n')\n      resolve([fixed, exit || 0])\n    })\n\n    function waitOut(): void {\n      if (out.length > 0) {\n        server.kill('SIGINT')\n      } else {\n        setTimeout(waitOut, 500)\n      }\n    }\n    if (kill) setTimeout(waitOut, 500)\n  })\n}\n\nfunction fakeProcess(argv: string[] = [], env: object = {}): any {\n  return { argv, env }\n}\n\nasync function checkOut(\n  name: string,\n  args?: string[],\n  opts?: SpawnOptions\n): Promise<void> {\n  let result = await check(name, args, opts, true)\n  let out = result[0]\n  let exit = result[1]\n  expect(out).toMatchSnapshot()\n  if (exit !== 0) {\n    throw new Error(`Fall with:\\n${out}`)\n  }\n}\n\nasync function checkError(\n  name: string,\n  args?: string[],\n  opts?: SpawnOptions\n): Promise<void> {\n  let result = await check(name, args, opts)\n  let out = result[0]\n  let exit = result[1]\n  expect(out).toMatchSnapshot()\n  expect(exit).toEqual(1)\n}\n\nafterEach(() => {\n  if (started) {\n    started.kill('SIGINT')\n    started = undefined\n  }\n})\n\nit('uses CLI args for options', () => {\n  let options = Server.loadOptions(\n    fakeProcess([\n      '',\n      '--port',\n      '1337',\n      '--host',\n      '192.168.1.1',\n      '--logger',\n      'json',\n      '--redis',\n      '//localhost'\n    ]),\n    {\n      minSubprotocol: 1,\n      subprotocol: 1\n    }\n  )\n\n  expect(options.host).toEqual('192.168.1.1')\n  expect(options.port).toEqual(1337)\n  expect(options.logger).toEqual('json')\n  expect(options.cert).toBeUndefined()\n  expect(options.key).toBeUndefined()\n  expect(options.redis).toEqual('//localhost')\n})\n\nit('uses env for options', () => {\n  let options = Server.loadOptions(\n    fakeProcess([], {\n      LOGUX_HOST: '127.0.1.1',\n      LOGUX_LOGGER: 'json',\n      LOGUX_PORT: '31337',\n      LOGUX_REDIS: '//localhost'\n    }),\n    {\n      minSubprotocol: 1,\n      subprotocol: 1\n    }\n  )\n\n  expect(options.host).toEqual('127.0.1.1')\n  expect(options.port).toEqual(31337)\n  expect(options.logger).toEqual('json')\n  expect(options.redis).toEqual('//localhost')\n})\n\nit('uses combined options', () => {\n  let options = Server.loadOptions(\n    fakeProcess(['', '--key', './key.pem'], { LOGUX_CERT: './cert.pem' }),\n    { minSubprotocol: 1, port: 31337, subprotocol: 1 }\n  )\n\n  expect(options.port).toEqual(31337)\n  expect(options.cert).toEqual('./cert.pem')\n  expect(options.key).toEqual('./key.pem')\n})\n\nit('uses arg, env, options in given priority', () => {\n  let options1 = Server.loadOptions(\n    fakeProcess(['', '--port', '31337'], { LOGUX_PORT: 21337 }),\n    {\n      minSubprotocol: 1,\n      port: 11337,\n      subprotocol: 1\n    }\n  )\n  let options2 = Server.loadOptions(fakeProcess([], { LOGUX_PORT: 21337 }), {\n    minSubprotocol: 1,\n    port: 11337,\n    subprotocol: 1\n  })\n  let options3 = Server.loadOptions(fakeProcess(), {\n    minSubprotocol: 1,\n    port: 11337,\n    subprotocol: 1\n  })\n\n  expect(options1.port).toEqual(31337)\n  expect(options2.port).toEqual(21337)\n  expect(options3.port).toEqual(11337)\n})\n\nit('destroys everything on exit', () => checkOut('destroy.js'))\n\nit('writes about unbind', async () => {\n  let result = await check('unbind.js', [], {}, true)\n  expect(result[0]).toMatchSnapshot()\n})\n\nit('shows uncatch errors', () => checkError('throw.js'))\n\nit('shows uncatch rejects', () => checkError('uncatch.js'))\n\nit('uses environment variables for config', () => {\n  return checkOut('options.js', [], {\n    env: {\n      ...process.env,\n      LOGUX_LOGGER: 'json',\n      LOGUX_PORT: '31337',\n      NODE_ENV: 'test'\n    }\n  })\n})\n\nit('uses logger param', () => checkOut('options.js', ['', '-l', 'json']))\n\nit('uses autoload modules', () => checkOut('autoload-modules.js'))\n\nit('uses autoload wrong export', () => checkError('autoload-error-modules.js'))\n\nit('uses .env cwd', async () => {\n  let result = await check(\n    'options.js',\n    [],\n    { cwd: join(ROOT, 'test/fixtures') },\n    true\n  )\n  expect(result[0]).toMatchSnapshot()\n})\n\nit('uses .env from root', () => checkOut('root.js'))\n\nit('shows help', async () => {\n  await checkOut('options.js', ['', '--help'], {\n    env: {\n      ...process.env,\n      NO_COLOR: '1'\n    }\n  })\n})\n\nit('shows help about port in use', async () => {\n  await start('eaddrinuse.js')\n  let result = await check('eaddrinuse.js')\n\n  expect(result[0]).toMatchSnapshot()\n})\n\nit('shows help about privileged port', () => checkError('eacces.js'))\n\nit('shows help about unknown option', () => checkError('unknown.js'))\n\nit('shows help about missed option', () => checkError('missed.js'))\n\nit('disables colors for constructor errors', () => {\n  return checkError('missed.js', [], {\n    env: {\n      ...process.env,\n      NODE_ENV: 'production'\n    }\n  })\n})\n\nit('uses logger param for constructor errors', () => {\n  return checkError('missed.js', ['', '-l', 'json'])\n})\n\nit('writes JSON log', () => checkOut('json.js'))\n\nit('has custom logger', () => checkOut('logger.js'))\n"
  },
  {
    "path": "server/types.ts",
    "content": "import { defineAction, type LoguxSubscribeAction } from '@logux/actions'\n\nimport { type Action, Server } from '../index.js'\n\nlet server = new Server<{ locale: string }>(\n  Server.loadOptions(process, {\n    minSubprotocol: 1,\n    root: '',\n    subprotocol: 1\n  })\n)\n\nserver.auth(({ token, userId }) => {\n  return token === userId\n})\n\nclass User {\n  id: string\n  name: string\n\n  constructor(id: string) {\n    this.id = id\n    this.name = 'name'\n  }\n\n  async save(): Promise<void> {}\n}\n\ntype UserRenameAction = {\n  name: string\n  type: 'user/rename'\n  userId: string\n} & Action\n\ntype UserSubscribeAction = {\n  fields?: ('email' | 'name')[]\n} & LoguxSubscribeAction\n\ntype UserData = {\n  user: User\n}\n\ntype UserParams = {\n  id: string\n}\n\nserver.type<UserRenameAction, UserData>('user/rename', {\n  access(ctx, action, meta) {\n    console.log(meta.id)\n    ctx.data.user = new User(action.userId)\n    return ctx.data.user.id === ctx.userId\n  },\n\n  async process(ctx, action) {\n    ctx.data.user.name = action.name\n    await ctx.data.user.save()\n  },\n\n  resend(ctx, action) {\n    return {\n      channels: [`user/${action.userId}`, `spellcheck/${ctx.headers.locale}`]\n    }\n  }\n})\n\nserver.channel<UserParams, UserData, UserSubscribeAction>('user/:id', {\n  access(ctx, action, meta) {\n    console.log(meta.id, action.since)\n    ctx.data.user = new User(ctx.params.id)\n    return ctx.data.user.id === ctx.userId\n  },\n  filter(ctx, action) {\n    return (cxt2, otherAction) => {\n      if (typeof action.fields !== 'undefined') {\n        return (\n          action.fields.includes('name') && otherAction.type === 'user/rename'\n        )\n      } else {\n        return true\n      }\n    }\n  },\n  async load(ctx) {\n    await ctx.sendBack(\n      {\n        name: ctx.data.user.name,\n        type: 'user/rename',\n        userId: ctx.data.user.id\n      },\n      {\n        status: 'processed'\n      }\n    )\n  }\n})\n\nserver.channel(/admin:\\d/, {\n  access(ctx, action, meta) {\n    console.log(meta.id, action.since)\n    return ctx.params[1] === ctx.userId\n  }\n})\n\nserver.on('connected', client => {\n  console.log(client.remoteAddress)\n})\n\nlet addUser = defineAction<{ type: 'user/remove'; userId: string }>(\n  'user/remove'\n)\n\nserver.type(addUser, {\n  access(ctx, action) {\n    return action.userId === ctx.userId\n  }\n})\n"
  },
  {
    "path": "server-client/index.d.ts",
    "content": "import type { ServerConnection, ServerNode } from '@logux/core'\n\nimport type { BaseServer } from '../base-server/index.js'\n\n/**\n * Logux client connected to server.\n *\n * ```js\n * const client = server.connected.get(0)\n * ```\n */\nexport class ServerClient {\n  /**\n   * Server, which received client.\n   */\n  app: BaseServer\n\n  /**\n   * Unique persistence machine ID.\n   * It will be undefined before correct authentication.\n   */\n  clientId?: string\n\n  /**\n   * The Logux wrapper to WebSocket connection.\n   *\n   * ```js\n   * console.log(client.connection.ws.upgradeReq.headers)\n   * ```\n   */\n  connection: ServerConnection\n\n  /**\n   * HTTP headers of WS connection.\n   *\n   * ```js\n   * client.httpHeaders['User-Agent']\n   * ```\n   */\n  httpHeaders: { [name: string]: string }\n\n  /**\n   * Client number used as `app.connected` key.\n   *\n   * ```js\n   * function stillConnected (client) {\n   *   return app.connected.has(client.key)\n   * }\n   * ```\n   */\n  key: string\n\n  /**\n   * Node instance to synchronize logs.\n   *\n   * ```js\n   * if (client.node.state === 'synchronized')\n   * ```\n   */\n  node: ServerNode\n\n  /**\n   * Unique node ID.\n   * It will be undefined before correct authentication.\n   */\n  nodeId?: string\n\n  /**\n   * Does server process some action from client.\n   *\n   * ```js\n   * console.log('Clients in processing:', clients.map(i => i.processing))\n   * ```\n   */\n  processing: boolean\n\n  /**\n   * Client IP address.\n   *\n   * ```js\n   * const clientCity = detectLocation(client.remoteAddress)\n   * ```\n   */\n  remoteAddress: string\n\n  /**\n   * User ID. It will be filled from client’s node ID.\n   * It will be undefined before correct authentication.\n   */\n  userId?: string\n\n  /**\n   * @param app The server.\n   * @param connection The Logux connection.\n   * @param key Client number used as `app.connected` key.\n   */\n  constructor(app: BaseServer, connection: ServerConnection, key: number)\n\n  /**\n   * Disconnect client.\n   */\n  destroy(): void\n}\n"
  },
  {
    "path": "server-client/index.js",
    "content": "import { LoguxError, parseId } from '@logux/core'\nimport cookie from 'cookie'\nimport fastq from 'fastq'\n\nimport { ALLOWED_META } from '../allowed-meta/index.js'\nimport { Context } from '../context/index.js'\nimport { filterMeta } from '../filter-meta/index.js'\nimport { FilteredNode } from '../filtered-node/index.js'\n\nasync function onSend(action, meta) {\n  return [action, filterMeta(meta)]\n}\n\nfunction reportDetails(client) {\n  return {\n    connectionId: client.key,\n    nodeId: client.nodeId,\n    subprotocol: client.node.remoteSubprotocol\n  }\n}\n\nfunction denyBack(app, clientId, action, meta) {\n  app.emitter.emit('report', 'denied', { actionId: meta.id })\n  let [undoAction, undoMeta] = app.buildUndo(action, meta, 'denied')\n  undoMeta.clients = (undoMeta.clients || []).concat([clientId])\n  app.log.add(undoAction, undoMeta)\n  app.debugActionError(meta, `Action \"${meta.id}\" was denied`)\n}\n\nasync function queueWorker(task, next) {\n  let { action, app, clientId, meta, onReceiveResolve, queue } = task\n  queue.next = next\n\n  let type = action.type\n  if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {\n    return onReceiveResolve([action, meta])\n  }\n\n  let processor = app.getProcessor(type)\n  if (!processor) {\n    app.internalUnknownType(action, meta)\n    return onReceiveResolve(false)\n  }\n\n  let ctx = app.createContext(action, meta)\n  try {\n    let result = await processor.access(ctx, action, meta)\n    if (app.unknownTypes[meta.id]) {\n      delete app.unknownTypes[meta.id]\n      app.finally(processor, ctx, action, meta)\n      return false\n    } else if (!result) {\n      app.finally(processor, ctx, action, meta)\n      denyBack(app, clientId, action, meta)\n      return onReceiveResolve(false)\n    } else {\n      return onReceiveResolve([action, meta])\n    }\n  } catch (e) {\n    app.undo(action, meta, 'error')\n    app.emitter.emit('error', e, action, meta)\n    app.finally(processor, ctx, action, meta)\n    return onReceiveResolve(false)\n  }\n}\n\nexport class ServerClient {\n  constructor(app, connection, key) {\n    this.app = app\n    this.userId = undefined\n    this.clientId = undefined\n    this.nodeId = undefined\n    this.processing = false\n    this.connection = connection\n    this.key = key.toString()\n    if (connection.ws) {\n      this.remoteAddress = connection.ws._socket.remoteAddress\n      this.httpHeaders = connection.ws.upgradeReq.headers\n    } else {\n      this.remoteAddress = '127.0.0.1'\n      this.httpHeaders = {}\n    }\n\n    let Node = app.options.Node || FilteredNode\n\n    this.node = new Node(this, app.nodeId, app.log, connection, {\n      auth: this.auth.bind(this),\n      onReceive: this.onReceive.bind(this),\n      onSend,\n      ping: app.options.ping,\n      subprotocol: app.options.subprotocol,\n      timeout: app.options.timeout\n    })\n    if (this.app.env === 'development') {\n      this.node.setLocalHeaders({ env: 'development' })\n    }\n\n    if (app.connectLoader) {\n      this.node.syncSinceQuery = async lastSynced => {\n        let context = new Context(app, this)\n        let entries = await app.connectLoader(context, lastSynced)\n        let added = entries.reduce((max, entry) => {\n          let meta = filterMeta(entry[1])\n          entry[1] = meta\n          return meta.added > max ? meta.added : max\n        }, 0)\n        return { added, entries }\n      }\n    }\n\n    this.node.catch(err => {\n      err.connectionId = this.key\n      this.app.emitter.emit('error', err)\n    })\n    this.node.on('state', () => {\n      if (!this.node.connected && !this.destroyed) this.destroy()\n    })\n    this.node.on('clientError', err => {\n      if (err.type !== 'wrong-credentials') {\n        err.connectionId = this.key\n        this.app.emitter.emit('clientError', err)\n      }\n    })\n\n    this.app.emitter.emit('connected', this)\n  }\n\n  async auth(nodeId, token) {\n    this.nodeId = nodeId\n    let { clientId, userId } = parseId(nodeId)\n    this.clientId = clientId\n    this.userId = userId\n\n    if (this.app.options.minSubprotocol) {\n      if (this.node.remoteSubprotocol < this.app.options.minSubprotocol) {\n        throw new LoguxError('wrong-subprotocol', {\n          supported: this.app.options.minSubprotocol,\n          used: this.node.remoteSubprotocol\n        })\n      }\n    }\n\n    if (nodeId === 'server' || userId === 'server') {\n      this.app.emitter.emit('unauthenticated', this, 0)\n      this.app.emitter.emit('report', 'unauthenticated', reportDetails(this))\n      return false\n    }\n\n    let ws = this.connection.ws\n    let headers = {}\n    if (ws && ws.upgradeReq && ws.upgradeReq.headers) {\n      headers = ws.upgradeReq.headers\n    }\n\n    let start = Date.now()\n    let result\n    try {\n      result = await this.app.authenticator({\n        client: this,\n        cookie: cookie.parse(headers.cookie || ''),\n        headers: this.node.remoteHeaders,\n        token,\n        userId: this.userId\n      })\n    } catch (e) {\n      if (e.name === 'LoguxError') {\n        /* c8 ignore next 1 */\n        throw e\n      } else {\n        e.nodeId = nodeId\n        this.app.emitter.emit('error', e)\n        result = false\n      }\n    }\n\n    if (this.app.isBruteforce(this.remoteAddress)) {\n      let e = new LoguxError('bruteforce')\n      e.nodeId = nodeId\n      this.app.emitter.emit('clientError', e)\n      result = false\n    }\n\n    if (result) {\n      let zombie = this.app.clientIds.get(this.clientId)\n      if (zombie) {\n        zombie.zombie = true\n        this.app.emitter.emit('report', 'zombie', { nodeId: zombie.nodeId })\n        zombie.destroy()\n      }\n      this.app.clientIds.set(this.clientId, this)\n      this.app.nodeIds.set(this.nodeId, this)\n      if (this.userId) {\n        if (!this.app.userIds.has(this.userId)) {\n          this.app.userIds.set(this.userId, [this])\n        } else {\n          this.app.userIds.get(this.userId).push(this)\n        }\n      }\n      this.app.emitter.emit('authenticated', this, Date.now() - start)\n      this.app.emitter.emit('report', 'authenticated', reportDetails(this))\n    } else {\n      this.app.emitter.emit('unauthenticated', this, Date.now() - start)\n      this.app.emitter.emit('report', 'unauthenticated', reportDetails(this))\n      this.app.rememberBadAuth(this.remoteAddress)\n    }\n    return result\n  }\n\n  destroy() {\n    this.destroyed = true\n    this.node.destroy()\n    if (this.userId) {\n      let users = this.app.userIds.get(this.userId)\n      if (users) {\n        users = users.filter(i => i !== this)\n        if (users.length === 0) {\n          this.app.userIds.delete(this.userId)\n        } else {\n          this.app.userIds.set(this.userId, users)\n        }\n      }\n    }\n    if (this.clientId) {\n      for (let channel in this.app.subscribers) {\n        let subscriber = this.app.subscribers[channel][this.nodeId]\n        if (subscriber) {\n          let action = { channel, type: 'logux/unsubscribe' }\n          let actionId = this.app.log.generateId()\n          let meta = { id: actionId, reasons: [], time: parseInt(actionId) }\n          this.app.performUnsubscribe(this.nodeId, action, meta)\n        }\n      }\n      this.app.clientIds.delete(this.clientId)\n      this.app.nodeIds.delete(this.nodeId)\n    }\n    if (!this.app.destroying) {\n      this.app.emitter.emit('disconnected', this)\n    }\n    this.app.connected.delete(this.key)\n  }\n\n  onReceive(action, meta) {\n    if (this.app.actionToQueue.has(meta.id)) {\n      return Promise.resolve(false)\n    }\n\n    let actionClientId = parseId(meta.id).clientId\n    let wrongUser = !this.clientId || this.clientId !== actionClientId\n    let wrongMeta = Object.keys(meta).some(i => !ALLOWED_META.includes(i))\n    if (wrongUser || wrongMeta) {\n      denyBack(this.app, this.clientId, action, meta)\n      return Promise.resolve(false)\n    }\n\n    return new Promise(resolve => {\n      let clientId = parseId(meta.id).clientId\n      let queueName = ''\n\n      let isChannel =\n        (action.type === 'logux/subscribe' ||\n          action.type === 'logux/unsubscribe') &&\n        action.channel\n\n      if (isChannel) {\n        for (let channel of this.app.channels) {\n          let pattern = channel.regexp || channel.pattern.regex\n          if (action.channel.match(pattern)) {\n            queueName = channel.queue\n            break\n          }\n        }\n      } else {\n        queueName = this.app.typeToQueue.get(action.type)\n      }\n\n      queueName = queueName || 'main'\n      let queueKey = `${clientId}/${queueName}`\n      let queue = this.app.queues.get(queueKey)\n\n      if (!queue) {\n        queue = fastq(queueWorker, 1)\n        this.app.queues.set(queueKey, queue)\n      }\n\n      if (!meta.subprotocol) {\n        meta.subprotocol = this.node.remoteSubprotocol\n      }\n\n      this.app.actionToQueue.set(meta.id, queueKey)\n      queue.push({\n        action,\n        app: this.app,\n        clientId,\n        meta,\n        onReceiveResolve: result => {\n          resolve(result)\n        },\n        queue\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "server-client/index.test.ts",
    "content": "import { LoguxNotFoundError } from '@logux/actions'\nimport {\n  type Action,\n  LoguxError,\n  type Message,\n  type Meta,\n  type ServerConnection,\n  type TestLog,\n  TestPair,\n  TestTime\n} from '@logux/core'\nimport { restoreAll, type Spy, spyOn } from 'nanospy'\nimport { setTimeout } from 'node:timers/promises'\nimport { afterEach, expect, it } from 'vitest'\n\nimport { FilteredNode } from '../filtered-node/index.js'\nimport {\n  BaseServer,\n  type BaseServerOptions,\n  ResponseError,\n  type ServerMeta\n} from '../index.js'\nimport { ServerClient } from './index.js'\n\nlet destroyable: { destroy(): void }[] = []\n\nfunction privateMethods(obj: object): any {\n  return obj\n}\n\nfunction getPair(client: ServerClient): TestPair {\n  return privateMethods(client.connection).pair\n}\n\nasync function sendTo(client: ServerClient, msg: Message): Promise<void> {\n  let pair = getPair(client)\n  pair.right.send(msg)\n  await pair.wait('right')\n}\n\nasync function connect(\n  client: ServerClient,\n  nodeId: string = '10:uuid',\n  details: object = {}\n): Promise<void> {\n  await client.connection.connect()\n  let protocol = client.node.localProtocol\n  await sendTo(client, [\n    'connect',\n    protocol,\n    nodeId,\n    0,\n    { subprotocol: 1, ...details }\n  ])\n}\n\nfunction createConnection(): ServerConnection {\n  let pair = new TestPair()\n  privateMethods(pair.left).ws = {\n    _socket: {\n      remoteAddress: '127.0.0.1'\n    },\n    upgradeReq: {\n      headers: { 'user-agent': 'browser' }\n    }\n  }\n  return pair.left as any\n}\n\nfunction createServer(\n  opts: Partial<BaseServerOptions> = {}\n): BaseServer<{ locale: string }, TestLog<ServerMeta>> {\n  opts.subprotocol = 1\n  opts.minSubprotocol = 1\n  opts.time = new TestTime()\n\n  let server = new BaseServer<{ locale: string }, TestLog<ServerMeta>>({\n    ...opts,\n    minSubprotocol: 1,\n    subprotocol: 1,\n    time: new TestTime()\n  })\n  server.auth(() => true)\n  server.on('preadd', (action, meta) => {\n    meta.reasons.push('test')\n  })\n\n  destroyable.push(server)\n\n  return server\n}\n\nfunction createReporter(opts: Partial<BaseServerOptions> = {}): {\n  app: BaseServer<{ locale: string }, TestLog<ServerMeta>>\n  names: string[]\n  reports: [string, any][]\n} {\n  let names: string[] = []\n  let reports: [string, any][] = []\n\n  let app = createServer(opts)\n  app.on('report', (name: string, details?: any) => {\n    names.push(name)\n    reports.push([name, details])\n  })\n  return { app, names, reports }\n}\n\nfunction createClient(app: BaseServer): ServerClient {\n  let lastClient: number = ++privateMethods(app).lastClient\n  let client = new ServerClient(app, createConnection(), lastClient)\n  app.connected.set(`${lastClient}`, client)\n  destroyable.push(client)\n  return client\n}\n\nasync function connectClient(\n  server: BaseServer,\n  nodeId = '10:uuid'\n): Promise<ServerClient> {\n  let client = createClient(server)\n  privateMethods(client.node).now = () => 0\n  await connect(client, nodeId)\n  return client\n}\n\nfunction sent(client: ServerClient): Message[] {\n  return getPair(client).leftSent\n}\n\nfunction sentNames(client: ServerClient): string[] {\n  return sent(client).map(i => i[0])\n}\n\nfunction actions(client: ServerClient): Action[] {\n  let received: Action[] = []\n  sent(client).forEach(i => {\n    if (i[0] === 'sync') {\n      for (let j = 2; j < i.length; j += 2) {\n        let action: Action = i[j] as any\n        if (action.type !== 'logux/processed') {\n          received.push(action)\n        }\n      }\n    }\n  })\n  return received\n}\n\nafterEach(() => {\n  restoreAll()\n  destroyable.forEach(i => {\n    i.destroy()\n  })\n  destroyable = []\n})\n\nit('uses server options', () => {\n  let app = createServer({\n    minSubprotocol: 1,\n    ping: 8000,\n    subprotocol: 1,\n    timeout: 16000\n  })\n  app.nodeId = 'server:x'\n  let client = new ServerClient(app, createConnection(), 1)\n\n  expect(client.node.options.subprotocol).toEqual(1)\n  expect(client.node.options.timeout).toEqual(16000)\n  expect(client.node.options.ping).toEqual(8000)\n  expect(client.node.localNodeId).toEqual('server:x')\n})\n\nit('saves connection', () => {\n  let connection = createConnection()\n  let client = new ServerClient(createServer(), connection, 1)\n  expect(client.connection).toBe(connection)\n})\n\nit('uses string key', () => {\n  let client = new ServerClient(createServer(), createConnection(), 1)\n  expect(client.key).toEqual('1')\n  expect(typeof client.key).toEqual('string')\n})\n\nit('has remote address shortcut', () => {\n  let client = new ServerClient(createServer(), createConnection(), 1)\n  expect(client.remoteAddress).toEqual('127.0.0.1')\n})\n\nit('has HTTP headers shortcut', () => {\n  let client = new ServerClient(createServer(), createConnection(), 1)\n  expect(client.httpHeaders['user-agent']).toEqual('browser')\n})\n\nit('has default remote address if ws param does not set', () => {\n  let pair = new TestPair()\n  let client = new ServerClient(createServer(), pair.left as any, 1)\n  expect(client.remoteAddress).toEqual('127.0.0.1')\n})\n\nit('reports about connection', () => {\n  let test = createReporter()\n  let fired: string[] = []\n  test.app.on('connected', client => {\n    fired.push(client.key)\n  })\n  new ServerClient(test.app, createConnection(), 1)\n  expect(test.reports).toEqual([\n    [\n      'connect',\n      {\n        connectionId: '1',\n        ipAddress: '127.0.0.1'\n      }\n    ]\n  ])\n  expect(fired).toEqual(['1'])\n})\n\nit('removes itself on destroy', async () => {\n  let test = createReporter()\n  let disconnectedKeys: string[] = []\n  test.app.on('disconnected', client => {\n    disconnectedKeys.push(client.key)\n  })\n  let lastPulledReports = new Set()\n  let pullNewReports = (): [string, any][] => {\n    let reports = test.reports\n    let result = reports.filter(x => !lastPulledReports.has(x))\n    lastPulledReports = new Set(reports)\n    return result\n  }\n\n  let client1 = createClient(test.app)\n  let client2 = createClient(test.app)\n\n  await client1.connection.connect()\n  await client2.connection.connect()\n  client1.node.remoteSubprotocol = 1\n  client2.node.remoteSubprotocol = 1\n  privateMethods(client1).auth('10:client1', {})\n  privateMethods(client2).auth('10:client2', {})\n  await setTimeout(1)\n  expect(pullNewReports()).toMatchObject([\n    ['connect', { connectionId: '1' }],\n    ['connect', { connectionId: '2' }],\n    ['authenticated', { connectionId: '1', nodeId: '10:client1' }],\n    ['authenticated', { connectionId: '2', nodeId: '10:client2' }]\n  ])\n\n  test.app.subscribers = {\n    'user/10': {\n      '10:client1': { filters: { '{}': true } },\n      '10:client2': { filters: { '{}': true } }\n    }\n  }\n  let unsubscribedClientNodeIds: string[] = []\n  test.app.on('unsubscribed', (action, meta, clientNodeId) => {\n    unsubscribedClientNodeIds.push(clientNodeId)\n    expect(test.app.nodeIds.get(clientNodeId)).toBeDefined()\n  })\n  client1.destroy()\n  await setTimeout(1)\n\n  expect(unsubscribedClientNodeIds).toEqual(['10:client1'])\n  expect(Array.from(test.app.userIds.keys())).toEqual(['10'])\n  expect(test.app.subscribers).toEqual({\n    'user/10': { '10:client2': { filters: { '{}': true } } }\n  })\n  expect(client1.connection.connected).toBe(false)\n  expect(pullNewReports()).toMatchObject([\n    ['unsubscribed', { channel: 'user/10' }],\n    ['disconnect', { nodeId: '10:client1' }]\n  ])\n\n  client2.destroy()\n  await setTimeout(1)\n\n  expect(unsubscribedClientNodeIds).toEqual(['10:client1', '10:client2'])\n  expect(pullNewReports()).toMatchObject([\n    ['unsubscribed', { channel: 'user/10' }],\n    ['disconnect', { nodeId: '10:client2' }]\n  ])\n  expect(test.app.connected.size).toEqual(0)\n  expect(test.app.clientIds.size).toEqual(0)\n  expect(test.app.nodeIds.size).toEqual(0)\n  expect(test.app.userIds.size).toEqual(0)\n  expect(test.app.subscribers).toEqual({})\n  expect(disconnectedKeys).toEqual(['1', '2'])\n})\n\nit('reports client ID before authentication', async () => {\n  let test = createReporter()\n  let client = createClient(test.app)\n\n  await client.connection.connect()\n  client.destroy()\n  expect(test.reports[1]).toEqual(['disconnect', { connectionId: '1' }])\n})\n\nit('does not report users disconnects on server destroy', async () => {\n  let test = createReporter()\n\n  let client = createClient(test.app)\n\n  await client.connection.connect()\n  test.app.destroy()\n  expect(test.app.connected.size).toEqual(0)\n  expect(client.connection.connected).toBe(false)\n  expect(test.names).toEqual(['connect', 'destroy'])\n  expect(test.reports[1]).toEqual(['destroy', undefined])\n})\n\nit('destroys on disconnect', async () => {\n  let client = createClient(createServer())\n  spyOn(client, 'destroy')\n  await client.connection.connect()\n  let pair = getPair(client)\n  pair.right.disconnect()\n  await pair.wait()\n\n  expect((client.destroy as any as Spy).callCount).toEqual(1)\n})\n\nit('reports on wrong authentication', async () => {\n  let test = createReporter()\n  test.app.auth(async () => false)\n  let client = new ServerClient(test.app, createConnection(), 1)\n  await connect(client)\n\n  expect(test.names).toEqual(['connect', 'unauthenticated', 'disconnect'])\n  expect(test.reports[1]).toEqual([\n    'unauthenticated',\n    {\n      connectionId: '1',\n      nodeId: '10:uuid',\n      subprotocol: 1\n    }\n  ])\n})\n\nit('reports about authentication error', async () => {\n  let test = createReporter()\n  let error = new Error('test')\n  let errors: Error[] = []\n  test.app.on('error', e => {\n    errors.push(e)\n  })\n  test.app.auth(() => {\n    throw error\n  })\n  let client = new ServerClient(test.app, createConnection(), 1)\n  await connect(client)\n\n  expect(test.names).toEqual([\n    'connect',\n    'error',\n    'unauthenticated',\n    'disconnect'\n  ])\n  expect(test.reports[1]).toEqual([\n    'error',\n    {\n      err: error,\n      nodeId: '10:uuid'\n    }\n  ])\n  expect(errors).toEqual([error])\n})\n\nit('blocks authentication bruteforce', async () => {\n  let test = createReporter()\n  test.app.auth(async () => false)\n\n  async function connectNext(num: number): Promise<void> {\n    let client = new ServerClient(test.app, createConnection(), num)\n    await connect(client, `${num}:uuid`)\n  }\n\n  await Promise.all([1, 2, 3, 4, 5].map(i => connectNext(i)))\n  expect(test.names.filter(i => i === 'disconnect')).toHaveLength(5)\n  expect(test.names.filter(i => i === 'unauthenticated')).toHaveLength(5)\n  expect(test.names.filter(i => i === 'clientError')).toHaveLength(2)\n  test.reports\n    .filter(i => i[0] === 'clientError')\n    .forEach(report => {\n      expect(report[1].err.type).toEqual('bruteforce')\n      expect(report[1].nodeId).toMatch(/(4|5):uuid/)\n    })\n  await setTimeout(3050)\n\n  await connectNext(6)\n\n  expect(test.names.filter(i => i === 'disconnect')).toHaveLength(6)\n  expect(test.names.filter(i => i === 'unauthenticated')).toHaveLength(6)\n  expect(test.names.filter(i => i === 'clientError')).toHaveLength(2)\n})\n\nit('reports on server in user name', async () => {\n  let test = createReporter()\n  test.app.auth(async () => true)\n  let client = new ServerClient(test.app, createConnection(), 1)\n  await connect(client, 'server:x')\n\n  expect(test.names).toEqual(['connect', 'unauthenticated', 'disconnect'])\n  expect(test.reports[1]).toEqual([\n    'unauthenticated',\n    {\n      connectionId: '1',\n      nodeId: 'server:x',\n      subprotocol: 1\n    }\n  ])\n})\n\nit('authenticates user', async () => {\n  let test = createReporter()\n  test.app.auth(async ({ client, headers, token, userId }) => {\n    return (\n      token === 'token' &&\n      userId === 'a' &&\n      client === testClient &&\n      headers.locale === 'fr'\n    )\n  })\n  let testClient = createClient(test.app)\n  testClient.node.remoteHeaders = { locale: 'fr' }\n\n  let authenticated: [ServerClient, number][] = []\n  test.app.on('authenticated', (...args) => {\n    authenticated.push(args)\n  })\n\n  await connect(testClient, 'a:b:uuid', { token: 'token' })\n\n  expect(testClient.userId).toEqual('a')\n  expect(testClient.clientId).toEqual('a:b')\n  expect(testClient.nodeId).toEqual('a:b:uuid')\n  expect(testClient.node.authenticated).toBe(true)\n  expect(test.app.nodeIds).toEqual(new Map([['a:b:uuid', testClient]]))\n  expect(test.app.clientIds).toEqual(new Map([['a:b', testClient]]))\n  expect(test.app.userIds).toEqual(new Map([['a', [testClient]]]))\n  expect(test.names).toEqual(['connect', 'authenticated'])\n  expect(test.reports[1]).toEqual([\n    'authenticated',\n    {\n      connectionId: '1',\n      nodeId: 'a:b:uuid',\n      subprotocol: 1\n    }\n  ])\n  expect(authenticated).toHaveLength(1)\n  expect(authenticated[0][0]).toBe(testClient)\n  expect(typeof authenticated[0][1]).toEqual('number')\n})\n\nit('supports non-promise authenticator', async () => {\n  let app = createServer()\n  app.auth(({ token }) => token === 'token')\n  let client = createClient(app)\n  await connect(client, '10:uuid', { token: 'token' })\n  expect(client.node.authenticated).toBe(true)\n})\n\nit('supports cookie based authenticator', async () => {\n  let app = createServer()\n  app.auth(({ cookie }) => cookie.token === 'good')\n  let client = createClient(app)\n  privateMethods(client.connection.ws).upgradeReq = {\n    headers: {\n      cookie: 'token=good; a=b'\n    }\n  }\n  await connect(client, '10:uuid')\n  expect(client.node.authenticated).toBe(true)\n})\n\nit('authenticates user without user name', async () => {\n  let app = createServer()\n  let client = createClient(app)\n\n  await connect(client, 'uuid', { token: 'token' })\n\n  expect(client.userId).toBeUndefined()\n  expect(app.userIds.size).toEqual(0)\n})\n\nit('reports about synchronization errors', async () => {\n  let test = createReporter()\n  let client = createClient(test.app)\n  await client.connection.connect()\n  sendTo(client, ['error', 'wrong-format'])\n  await getPair(client).wait()\n\n  expect(test.names).toEqual(['connect', 'error'])\n  let err = new LoguxError('wrong-format', undefined, true)\n  // @ts-expect-error Unofficial object extend for internal needs\n  err.connectionId = '1'\n  expect(test.reports[1]).toEqual(['error', { connectionId: '1', err }])\n})\n\nit('checks subprotocol', async () => {\n  let test = createReporter()\n  let client = createClient(test.app)\n  await connect(client, '10:uuid', { subprotocol: 0 })\n\n  expect(test.names).toEqual(['connect', 'clientError', 'disconnect'])\n  let err = new LoguxError('wrong-subprotocol', {\n    supported: 1,\n    used: 0\n  })\n  // @ts-expect-error Unofficial object extend for internal needs\n  err.connectionId = '1'\n  expect(test.reports[1]).toEqual(['clientError', { connectionId: '1', err }])\n})\n\nit('sends server environment in development', async () => {\n  let app = createServer({ env: 'development' })\n  let client = await connectClient(app)\n  let headers = sent(client).find(i => i[0] === 'headers')\n  expect(headers).toEqual(['headers', { env: 'development' }])\n})\n\nit('does not send server environment in production', async () => {\n  let app = createServer({ env: 'production' })\n  app.auth(async () => true)\n\n  let client = await connectClient(app)\n  expect(sent(client)[0][4]).toEqual({ subprotocol: 1 })\n})\n\nit('disconnects zombie', async () => {\n  let test = createReporter()\n\n  let client1 = createClient(test.app)\n  let client2 = createClient(test.app)\n\n  await client1.connection.connect()\n  client1.node.remoteSubprotocol = 1\n  privateMethods(client1).auth('10:client:a', {})\n\n  await client2.connection.connect()\n  client2.node.remoteSubprotocol = 1\n  privateMethods(client2).auth('10:client:b', {})\n  await setTimeout(0)\n\n  expect(Array.from(test.app.connected.keys())).toEqual([client2.key])\n  expect(test.names).toEqual([\n    'connect',\n    'connect',\n    'authenticated',\n    'zombie',\n    'authenticated'\n  ])\n  expect(test.reports[3]).toEqual(['zombie', { nodeId: '10:client:a' }])\n})\n\nit('checks action access', async () => {\n  let test = createReporter()\n  let finalled = 0\n  test.app.type('FOO', {\n    access: () => false,\n    finally() {\n      finalled += 1\n    }\n  })\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'FOO' },\n    { id: [1, '10:uuid', 0], time: 1 }\n  ])\n\n  expect(test.names).toEqual(['connect', 'authenticated', 'denied', 'add'])\n  expect(test.app.log.actions()).toEqual([\n    {\n      action: { type: 'FOO' },\n      id: '1 10:uuid 0',\n      reason: 'denied',\n      type: 'logux/undo'\n    }\n  ])\n  expect(finalled).toEqual(1)\n})\n\nit('checks action creator', async () => {\n  let test = createReporter()\n  test.app.type('GOOD', { access: () => true })\n  test.app.type('BAD', { access: () => true })\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'GOOD' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { type: 'BAD' },\n    { id: [2, '1:uuid', 0], time: 2 }\n  ])\n\n  expect(test.names).toEqual([\n    'connect',\n    'authenticated',\n    'denied',\n    'add',\n    'add',\n    'add'\n  ])\n  expect(test.reports[2]).toEqual(['denied', { actionId: '2 1:uuid 0' }])\n  expect(test.reports[4][1].meta.id).toEqual('1 10:uuid 0')\n  expect(test.app.log.actions()).toEqual([\n    { type: 'GOOD' },\n    {\n      action: { type: 'BAD' },\n      id: '2 1:uuid 0',\n      reason: 'denied',\n      type: 'logux/undo'\n    },\n    { id: '1 10:uuid 0', type: 'logux/processed' }\n  ])\n})\n\nit('allows subscribe and unsubscribe actions', async () => {\n  let test = createReporter()\n  test.app.channel('a', { access: () => true })\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    3,\n    { channel: 'a', type: 'logux/subscribe' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { channel: 'b', type: 'logux/unsubscribe' },\n    { id: [2, '10:uuid', 0], time: 2 },\n    { type: 'logux/undo' },\n    { id: [3, '10:uuid', 0], time: 3 }\n  ])\n\n  expect(test.names[8]).toEqual('unknownType')\n  expect(test.reports[8][1].actionId).toEqual('3 10:uuid 0')\n  expect(test.names).toContain('unsubscribed')\n  expect(test.names).toContain('subscribed')\n})\n\nit('checks action meta', async () => {\n  let test = createReporter()\n  test.app.type('GOOD', { access: () => true }, { queue: '1' })\n  test.app.type('BAD', { access: () => true }, { queue: '2' })\n\n  test.app.log.generateId()\n  test.app.log.generateId()\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'BAD' },\n    { id: [1, '10:uuid', 0], status: 'processed', time: 1 },\n    { type: 'GOOD' },\n    {\n      id: [2, '10:uuid', 0],\n      subprotocol: 1,\n      time: 3\n    }\n  ])\n\n  expect(test.app.log.actions()).toEqual([\n    { type: 'GOOD' },\n    {\n      action: { type: 'BAD' },\n      id: '1 10:uuid 0',\n      reason: 'denied',\n      type: 'logux/undo'\n    },\n    { id: '2 10:uuid 0', type: 'logux/processed' }\n  ])\n  expect(test.names).toEqual([\n    'connect',\n    'authenticated',\n    'denied',\n    'add',\n    'add',\n    'add'\n  ])\n  expect(test.reports[2][1].actionId).toEqual('1 10:uuid 0')\n  expect(test.reports[4][1].meta.id).toEqual('2 10:uuid 0')\n})\n\nit('ignores unknown action types', async () => {\n  let test = createReporter()\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'UNKNOWN' },\n    { id: [1, '10:uuid', 0], time: 1 }\n  ])\n\n  expect(test.app.log.actions()).toEqual([\n    {\n      action: { type: 'UNKNOWN' },\n      id: '1 10:uuid 0',\n      reason: 'unknownType',\n      type: 'logux/undo'\n    }\n  ])\n  expect(test.names).toEqual(['connect', 'authenticated', 'unknownType', 'add'])\n  expect(test.reports[2]).toEqual([\n    'unknownType',\n    {\n      actionId: '1 10:uuid 0',\n      type: 'UNKNOWN'\n    }\n  ])\n})\n\nit('checks user access for action', async () => {\n  let test = createReporter({ env: 'development' })\n  type FooAction = {\n    bar: boolean\n    type: 'FOO'\n  }\n  test.app.type<FooAction>('FOO', {\n    async access(ctx, action, meta) {\n      expect(ctx.userId).toEqual('10')\n      expect(ctx.subprotocol).toEqual(1)\n      expect(meta.id).toBeDefined()\n      return action.bar\n    }\n  })\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { bar: true, type: 'FOO' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { type: 'FOO' },\n    { id: [1, '10:uuid', 1], time: 1 }\n  ])\n  await setTimeout(50)\n  expect(test.app.log.actions()).toEqual([\n    { bar: true, type: 'FOO' },\n    { id: '1 10:uuid 0', type: 'logux/processed' },\n    {\n      action: { type: 'FOO' },\n      id: '1 10:uuid 1',\n      reason: 'denied',\n      type: 'logux/undo'\n    }\n  ])\n  expect(test.names).toEqual([\n    'connect',\n    'authenticated',\n    'add',\n    'denied',\n    'add',\n    'add'\n  ])\n  expect(test.reports.find(i => i[0] === 'denied')![1].actionId).toEqual(\n    '1 10:uuid 1'\n  )\n  expect(sent(client).find(i => i[0] === 'debug')).toEqual([\n    'debug',\n    'error',\n    'Action \"1 10:uuid 1\" was denied'\n  ])\n})\n\nit('takes subprotocol from action meta', async () => {\n  let app = createServer()\n  let subprotocols: number[] = []\n  app.type('FOO', {\n    access: () => true,\n    process(ctx) {\n      subprotocols.push(ctx.subprotocol)\n    }\n  })\n\n  let client = await connectClient(app)\n  app.log.add({ type: 'FOO' }, { id: `1 ${client.nodeId} 0`, subprotocol: 1 })\n  await setTimeout(1)\n\n  expect(subprotocols).toEqual([1])\n})\n\nit('reports about errors in access callback', async () => {\n  let err = new Error('test')\n\n  let test = createReporter()\n  let finalled = 0\n  test.app.type('FOO', {\n    access() {\n      throw err\n    },\n    finally() {\n      finalled += 1\n    }\n  })\n\n  let throwed\n  test.app.on('error', e => {\n    throwed = e\n  })\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { bar: true, type: 'FOO' },\n    { id: [1, '10:uuid', 0], time: 1 }\n  ])\n\n  expect(test.app.log.actions()).toEqual([\n    {\n      action: { bar: true, type: 'FOO' },\n      id: '1 10:uuid 0',\n      reason: 'error',\n      type: 'logux/undo'\n    }\n  ])\n  expect(test.names).toEqual(['connect', 'authenticated', 'error', 'add'])\n  expect(test.reports[2]).toEqual([\n    'error',\n    {\n      actionId: '1 10:uuid 0',\n      err\n    }\n  ])\n  expect(throwed).toEqual(err)\n  expect(finalled).toEqual(1)\n})\n\nit('adds resend keys', async () => {\n  let test = createReporter()\n  test.app.type('FOO', {\n    access: () => true,\n    resend(ctx, action, meta) {\n      expect(ctx.nodeId).toEqual('10:uuid')\n      expect(action.type).toEqual('FOO')\n      expect(meta.id).toEqual('1 10:uuid 0')\n      return {\n        channels: ['a'],\n        clients: ['1:client'],\n        nodes: ['1:client:other'],\n        users: ['1']\n      }\n    }\n  })\n  test.app.type('EMPTY', {\n    access: () => true\n  })\n\n  test.app.log.generateId()\n  test.app.log.generateId()\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'FOO' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { type: 'EMPTY' },\n    { id: [2, '10:uuid', 0], time: 2 }\n  ])\n\n  expect(test.app.log.actions()).toEqual([\n    { type: 'FOO' },\n    { type: 'EMPTY' },\n    { id: '1 10:uuid 0', type: 'logux/processed' },\n    { id: '2 10:uuid 0', type: 'logux/processed' }\n  ])\n  expect(test.names).toEqual([\n    'connect',\n    'authenticated',\n    'add',\n    'add',\n    'add',\n    'add'\n  ])\n  expect(test.reports[2][1].action.type).toEqual('FOO')\n  expect(test.reports[2][1].meta.nodes).toEqual(['1:client:other'])\n  expect(test.reports[2][1].meta.clients).toEqual(['1:client'])\n  expect(test.reports[2][1].meta.channels).toEqual(['a'])\n  expect(test.reports[2][1].meta.users).toEqual(['1'])\n  expect(test.reports[4][1].action.type).toEqual('EMPTY')\n  expect(test.reports[4][1].meta.users).not.toBeDefined()\n})\n\nit('has channel resend shortcut', async () => {\n  let app = createServer()\n  app.type('FOO', {\n    access: () => true,\n    resend() {\n      return 'bar'\n    }\n  })\n  app.type('FOOS', {\n    access: () => true,\n    resend() {\n      return ['bar1', 'bar2']\n    }\n  })\n\n  let client = await connectClient(app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'FOO' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { type: 'FOOS' },\n    { id: [2, '10:uuid', 0], time: 2 }\n  ])\n\n  expect(app.log.actions()).toEqual([\n    { type: 'FOO' },\n    { id: '1 10:uuid 0', type: 'logux/processed' },\n    { type: 'FOOS' },\n    { id: '2 10:uuid 0', type: 'logux/processed' }\n  ])\n  expect(app.log.entries()[0][1].channels).toEqual(['bar'])\n  expect(app.log.entries()[2][1].channels).toEqual(['bar1', 'bar2'])\n})\n\nit('sends old actions by node ID', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add({ type: 'A' }, { id: '2 server:x 0', nodes: ['10:uuid'] })\n  let client = await connectClient(app)\n\n  sendTo(client, ['synced', 2])\n  await client.node.waitFor('synchronized')\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('sends new actions by node ID', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  let client = await connectClient(app)\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add({ type: 'A' }, { id: '2 server:x 0', nodes: ['10:uuid'] })\n  sendTo(client, ['synced', 2])\n  await setTimeout(10)\n\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('sends old actions by client ID', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add(\n    { type: 'A' },\n    { clients: ['10:client'], id: '2 server:x 0' }\n  )\n  let client = await connectClient(app, '10:client:uuid')\n\n  sendTo(client, ['synced', 2])\n  await client.node.waitFor('synchronized')\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('sends new actions by client ID', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  let client = await connectClient(app, '10:client:uuid')\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add(\n    { type: 'A' },\n    { clients: ['10:client'], id: '2 server:x 0' }\n  )\n  sendTo(client, ['synced', 2])\n  await setTimeout(1)\n\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('does not send old action on client excluding', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add(\n    { type: 'A' },\n    { excludeClients: ['10:client'], id: '2 server:x 0', users: ['10'] }\n  )\n  let client = await connectClient(app, '10:client:uuid')\n\n  sendTo(client, ['synced', 2])\n  await client.node.waitFor('synchronized')\n  expect(sentNames(client)).toEqual(['connected'])\n})\n\nit('sends old actions by user', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add({ type: 'A' }, { id: '2 server:x 0', users: ['10'] })\n  let client = await connectClient(app)\n\n  sendTo(client, ['synced', 2])\n  await client.node.waitFor('synchronized')\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('sends new actions by user', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n\n  let client = await connectClient(app)\n  await app.log.add({ type: 'A' }, { id: '1 server:x 0' })\n  await app.log.add({ type: 'A' }, { id: '2 server:x 0', users: ['10'] })\n  sendTo(client, ['synced', 2])\n  await setTimeout(10)\n\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('sends new actions by channel', async () => {\n  let app = createServer()\n  app.type('FOO', { access: () => true })\n  app.type('BAR', { access: () => true })\n\n  let client = await connectClient(app)\n  app.subscribers.foo = {\n    '10:uuid': { filters: { '{}': true } }\n  }\n  app.subscribers.bar = {\n    '10:uuid': {\n      filters: {\n        '{}': (ctx, action, meta) => {\n          expect(meta.id).toContain(' server:x ')\n          expect(ctx.isServer).toBe(true)\n          return privateMethods(action).secret !== true\n        }\n      }\n    }\n  }\n  await app.log.add({ type: 'FOO' }, { id: '1 server:x 0' })\n  await app.log.add({ type: 'FOO' }, { channels: ['foo'], id: '2 server:x 0' })\n  await app.log.add(\n    { secret: true, type: 'BAR' },\n    {\n      channels: ['bar'],\n      id: '3 server:x 0'\n    }\n  )\n  await app.log.add({ type: 'BAR' }, { channels: ['bar'], id: '4 server:x 0' })\n  sendTo(client, ['synced', 2])\n  sendTo(client, ['synced', 4])\n  await client.node.waitFor('synchronized')\n  await setTimeout(1)\n\n  expect(sentNames(client)).toEqual(['connected', 'sync', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    2,\n    { type: 'FOO' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n  expect(sent(client)[2]).toEqual([\n    'sync',\n    4,\n    { type: 'BAR' },\n    { id: [4, 'server:x', 0], time: 4 }\n  ])\n})\n\nit('excludes client from channel', async () => {\n  let app = createServer()\n  app.type('FOO', { access: () => true })\n\n  let client1 = await connectClient(app, '10:1:uuid')\n  let client2 = await connectClient(app, '10:2:uuid')\n  app.subscribers.foo = {\n    '10:1:uuid': { filters: { '{}': true } },\n    '10:2:uuid': { filters: { '{}': true } }\n  }\n  await app.log.add(\n    { type: 'FOO' },\n    { channels: ['foo'], excludeClients: ['10:1'], id: '2 server:x 0' }\n  )\n  await setTimeout(10)\n\n  expect(sentNames(client1)).toEqual(['connected'])\n  expect(sentNames(client2)).toEqual(['connected', 'sync'])\n  expect(sent(client2)[1]).toEqual([\n    'sync',\n    1,\n    { type: 'FOO' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('works with channel according client ID', async () => {\n  let app = createServer()\n  app.type('FOO', { access: () => true })\n  app.type('BAR', { access: () => true })\n\n  let client = await connectClient(app, '10:uuid:a')\n  app.subscribers.foo = {\n    '10:uuid:b': { filters: { '{}': true } },\n    '10:uuid:c': { filters: { '{}': true } }\n  }\n  await app.log.add({ type: 'FOO' }, { channels: ['foo'], id: '2 server:x 0' })\n  sendTo(client, ['synced', 1])\n  await setTimeout(10)\n\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    1,\n    { type: 'FOO' },\n    { id: [2, 'server:x', 0], time: 2 }\n  ])\n})\n\nit('sends old action only once', async () => {\n  let app = createServer()\n  app.type('FOO', { access: () => true })\n\n  await app.log.add(\n    { type: 'FOO' },\n    {\n      clients: ['10:uuid', '10:uuid'],\n      id: '1 server:x 0',\n      nodes: ['10:uuid', '10:uuid'],\n      users: ['10', '10']\n    }\n  )\n  let client = await connectClient(app)\n\n  sendTo(client, ['synced', 2])\n  await client.node.waitFor('synchronized')\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n  expect(sent(client)[1]).toEqual([\n    'sync',\n    1,\n    { type: 'FOO' },\n    { id: [1, 'server:x', 0], time: 1 }\n  ])\n})\n\nit('sends debug back on unknown type', async () => {\n  let app = createServer({ env: 'development' })\n  let client1 = await connectClient(app)\n  let client2 = await connectClient(app, '20:uuid')\n  app.log.add({ type: 'UNKNOWN' }, { id: '1 server:x 0' })\n  app.log.add({ type: 'UNKNOWN' }, { id: '2 10:uuid 0' })\n  await getPair(client1).wait('right')\n\n  expect(sent(client1).find(i => i[0] === 'debug')).toEqual([\n    'debug',\n    'error',\n    'Action with unknown type UNKNOWN'\n  ])\n  expect(sentNames(client2)).toEqual(['headers', 'connected'])\n})\n\nit('does not send debug back on unknown type in production', async () => {\n  let app = createServer({ env: 'production' })\n  let client = await connectClient(app)\n  await app.log.add({ type: 'U' }, { id: '1 10:uuid 0' })\n  await getPair(client).wait('right')\n\n  expect(sentNames(client)).toEqual(['connected', 'sync'])\n})\n\nit('decompress subprotocol', async () => {\n  let app = createServer({ env: 'production' })\n  app.type('A', { access: () => true })\n\n  app.log.generateId()\n  app.log.generateId()\n\n  let client = await connectClient(app)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'A' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { type: 'A' },\n    { id: [2, '10:uuid', 0], subprotocol: 2, time: 2 }\n  ])\n\n  expect(app.log.entries()[0][1].subprotocol).toEqual(1)\n  expect(app.log.entries()[1][1].subprotocol).toEqual(2)\n})\n\nit('has custom processor for unknown type', async () => {\n  let test = createReporter()\n  let calls: string[] = []\n  test.app.otherType({\n    access() {\n      calls.push('access')\n      return true\n    },\n    process() {\n      calls.push('process')\n    }\n  })\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'UNKOWN' },\n    { id: [1, '10:uuid', 0], time: 1 }\n  ])\n\n  expect(test.names).toEqual(['connect', 'authenticated', 'add', 'add'])\n  expect(calls).toEqual(['access', 'process'])\n})\n\nit('allows to reports about unknown type in custom processor', async () => {\n  let test = createReporter()\n  let calls: string[] = []\n  test.app.otherType({\n    access(ctx, action, meta) {\n      calls.push('access')\n      test.app.unknownType(action, meta)\n      return true\n    },\n    process() {\n      calls.push('process')\n    }\n  })\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'UNKOWN' },\n    { id: [1, '10:uuid', 0], time: 1 }\n  ])\n\n  expect(test.names).toEqual(['connect', 'authenticated', 'unknownType', 'add'])\n  expect(calls).toEqual(['access'])\n})\n\nit('allows to use different node ID', async () => {\n  let app = createServer()\n  let calls = 0\n  app.type('A', {\n    access(ctx, action, meta) {\n      expect(ctx.nodeId).toEqual('10:client:other')\n      expect(meta.id).toEqual('1 10:client:other 0')\n      calls += 1\n      return true\n    }\n  })\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'A' },\n    { id: [1, '10:client:other', 0], time: 1 }\n  ])\n\n  expect(calls).toEqual(1)\n  expect(app.log.entries()[1][0].type).toEqual('logux/processed')\n  expect(app.log.entries()[1][1].clients).toEqual(['10:client'])\n})\n\nit('allows to use different node ID only with same client ID', async () => {\n  let test = createReporter()\n  let client = await connectClient(test.app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'A' },\n    { id: [1, '10:clnt:uuid', 0], time: 1 }\n  ])\n\n  expect(test.names).toEqual(['connect', 'authenticated', 'denied', 'add'])\n})\n\nit('has finally callback', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  let errors: string[] = []\n  app.on('error', e => {\n    errors.push(e.message)\n  })\n  app.type(\n    'A',\n    {\n      access: () => true,\n      finally() {\n        calls.push('A')\n      }\n    },\n    { queue: 'A' }\n  )\n  app.type(\n    'B',\n    {\n      access: () => true,\n      finally() {\n        calls.push('B')\n      },\n      process: () => {}\n    },\n    { queue: 'B' }\n  )\n  app.type(\n    'C',\n    {\n      access: () => true,\n      finally() {\n        calls.push('C')\n      },\n      resend() {\n        throw new Error('C')\n      }\n    },\n    { queue: 'C' }\n  )\n  app.type(\n    'D',\n    {\n      access() {\n        throw new Error('D')\n      },\n      finally() {\n        calls.push('D')\n      }\n    },\n    { queue: 'D' }\n  )\n  app.type(\n    'E',\n    {\n      access: () => true,\n      finally() {\n        calls.push('E')\n        throw new Error('EE')\n      },\n      process() {\n        throw new Error('E')\n      }\n    },\n    { queue: 'E' }\n  )\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    5,\n    { type: 'A' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'B' },\n    { id: [2, '10:client:other', 0], time: 1 },\n    { type: 'C' },\n    { id: [3, '10:client:other', 0], time: 1 },\n    { type: 'D' },\n    { id: [4, '10:client:other', 0], time: 1 },\n    { type: 'E' },\n    { id: [5, '10:client:other', 0], time: 1 }\n  ])\n\n  expect(calls).toEqual(['D', 'C', 'A', 'E', 'B'])\n  expect(errors).toEqual(['D', 'C', 'E', 'EE'])\n})\n\nit('sends error to author', async () => {\n  let app = createServer()\n  app.type('A', { access: () => true })\n  let client1 = await connectClient(app, '10:1:uuid')\n  let client2 = await connectClient(app, '10:2:uuid')\n\n  await sendTo(client2, [\n    'sync',\n    1,\n    { type: 'A' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(1)\n\n  expect(sent(client1)).toHaveLength(1)\n  expect(sent(client2)).toHaveLength(3)\n})\n\nit('does not resend actions back', async () => {\n  let app = createServer()\n\n  app.type('A', {\n    access: () => true,\n    resend: () => ({ users: ['10'] })\n  })\n  app.type('B', {\n    access: () => true,\n    resend: () => ({ channels: ['all'] })\n  })\n  app.channel('all', { access: () => true })\n\n  let client1 = await connectClient(app, '10:1:uuid')\n  let client2 = await connectClient(app, '10:2:uuid')\n\n  await sendTo(client1, [\n    'sync',\n    1,\n    { channel: 'all', type: 'logux/subscribe' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await sendTo(client2, [\n    'sync',\n    1,\n    { channel: 'all', type: 'logux/subscribe' },\n    { id: [1, '10:2:uuid', 0], time: 1 }\n  ])\n\n  await sendTo(client1, [\n    'sync',\n    4,\n    { type: 'A' },\n    { id: [2, '10:1:uuid', 0], time: 2 },\n    { type: 'B' },\n    { id: [3, '10:1:uuid', 0], time: 3 }\n  ])\n  await setTimeout(10)\n\n  expect(actions(client1)).toEqual([])\n  expect(actions(client2)).toEqual([{ type: 'A' }, { type: 'B' }])\n})\n\nit('keeps context', async () => {\n  let app = createServer()\n  app.type<Action, { a: number }>('A', {\n    access(ctx) {\n      ctx.data.a = 1\n      return true\n    },\n    finally(ctx) {\n      expect(ctx.data.a).toEqual(1)\n    },\n    process(ctx) {\n      expect(ctx.data.a).toEqual(1)\n    }\n  })\n\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'A' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(1)\n\n  expect(sent(client)[2][2].type).toEqual('logux/processed')\n})\n\nit('uses resend for own actions', async () => {\n  let app = createServer()\n  app.type('FOO', {\n    access: () => false,\n    resend: () => ({ channel: 'foo' })\n  })\n  app.channel('foo', {\n    access: () => true\n  })\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { channel: 'foo', type: 'logux/subscribe' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(10)\n\n  app.log.add({ type: 'FOO' })\n  await setTimeout(10)\n  expect(app.log.entries()[2][1].channels).toEqual(['foo'])\n  expect(sent(client)[3][2]).toEqual({ type: 'FOO' })\n\n  app.log.add({ type: 'FOO' }, { status: 'processed' })\n  await setTimeout(10)\n  expect(app.log.entries()[3][1].channels).not.toBeDefined()\n})\n\nit('does not duplicate channel load actions', async () => {\n  let app = createServer()\n  app.type('FOO', {\n    access: () => true,\n    resend: () => ({ channel: 'foo' })\n  })\n  app.channel('foo', {\n    access: () => true,\n    async load(ctx) {\n      await ctx.sendBack({ type: 'FOO' })\n    }\n  })\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { channel: 'foo', type: 'logux/subscribe' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(10)\n\n  function meta(time: number): object {\n    return { id: time, subprotocol: 1, time }\n  }\n\n  expect(sent(client).slice(1)).toEqual([\n    ['synced', 1],\n    ['sync', 2, { type: 'FOO' }, meta(1)],\n    ['sync', 3, { id: '1 10:1:uuid 0', type: 'logux/processed' }, meta(2)]\n  ])\n})\n\nit('allows to return actions', async () => {\n  let app = createServer()\n  app.channel('a', {\n    access: () => true,\n    load() {\n      return { type: 'A' }\n    }\n  })\n  app.channel('b', {\n    access: () => true,\n    load() {\n      return [{ type: 'B' }]\n    }\n  })\n  app.channel('c', {\n    access: () => true,\n    load() {\n      return [[{ type: 'C' }, { time: 100 }]]\n    }\n  })\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { channel: 'a', type: 'logux/subscribe' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'b', type: 'logux/subscribe' },\n    { id: [2, '10:1:uuid', 0], time: 2 }\n  ])\n  await sendTo(client, [\n    'sync',\n    3,\n    { channel: 'c', type: 'logux/subscribe' },\n    { id: [3, '10:1:uuid', 0], time: 3 }\n  ])\n  await setTimeout(50)\n\n  function meta(time: number): object {\n    return { id: time, subprotocol: 1, time }\n  }\n\n  expect(sent(client).slice(1)).toEqual([\n    ['synced', 1],\n    ['sync', 2, { type: 'A' }, meta(1)],\n    ['sync', 3, { id: '1 10:1:uuid 0', type: 'logux/processed' }, meta(2)],\n    ['synced', 2],\n    ['sync', 5, { type: 'B' }, meta(3)],\n    ['sync', 6, { id: '2 10:1:uuid 0', type: 'logux/processed' }, meta(4)],\n    ['synced', 3],\n    ['sync', 8, { type: 'C' }, { ...meta(5), time: 100 }],\n    ['sync', 9, { id: '3 10:1:uuid 0', type: 'logux/processed' }, meta(6)]\n  ])\n})\n\nit('does not process send-back actions', async () => {\n  let app = createServer()\n  app.channel('a', {\n    access: () => true,\n    load() {\n      return { data: 'load', type: 'A' }\n    }\n  })\n\n  let processed: string[] = []\n  let resended: string[] = []\n  app.type('A', {\n    access: () => true,\n    process(ctx, action) {\n      processed.push(action.data)\n    },\n    resend(ctx, action) {\n      resended.push(action.data)\n      return {}\n    }\n  })\n\n  app.log.add({ data: 'server', type: 'A' })\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { data: 'client', type: 'A' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'a', type: 'logux/subscribe' },\n    { id: [2, '10:1:uuid', 0], time: 2 }\n  ])\n  await setTimeout(10)\n\n  expect(resended).toEqual(['server', 'client'])\n  expect(processed).toEqual(['server', 'client'])\n})\n\nit('restores actions with old ID from history', async () => {\n  let app = createServer()\n  app.on('preadd', (action, meta) => {\n    meta.reasons = []\n  })\n  let history: [Action, ServerMeta][] = []\n  app.channel('a', {\n    access: () => true,\n    load() {\n      return history\n    }\n  })\n  app.type('A', {\n    access: () => true,\n    process(ctx, action, meta) {\n      history.push([action, meta])\n    }\n  })\n\n  let client1 = await connectClient(app, '10:1:uuid')\n  await sendTo(client1, [\n    'sync',\n    1,\n    { type: 'A' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n\n  let client2 = await connectClient(app, '10:1:other')\n  await sendTo(client2, [\n    'sync',\n    2,\n    { channel: 'a', type: 'logux/subscribe' },\n    { id: [2, '10:1:uuid', 0], time: 2 }\n  ])\n  await setTimeout(10)\n  expect(actions(client2)).toEqual([{ type: 'A' }])\n})\n\nit('has shortcut to access and process in one callback', async () => {\n  let app = createServer()\n  app.log.keepActions()\n\n  app.type('FOO', {\n    async accessAndProcess(ctx, action, meta) {\n      expect(typeof meta.id).toEqual('string')\n      expect(action.type).toEqual('FOO')\n      await ctx.sendBack({ type: 'REFOO' })\n    }\n  })\n  app.otherType({\n    async accessAndProcess(ctx, action, meta) {\n      expect(typeof meta.id).toEqual('string')\n      expect(typeof action.type).toEqual('string')\n      if (action.type === 'BAR') {\n        await ctx.sendBack({ type: 'REBAR' })\n      }\n    }\n  })\n  app.channel('foo', {\n    async accessAndLoad(ctx, action, meta) {\n      expect(typeof meta.id).toEqual('string')\n      expect(action.type).toEqual('logux/subscribe')\n      return { type: 'FOO:load' }\n    }\n  })\n  app.otherChannel({\n    accessAndLoad(ctx, action, meta) {\n      expect(typeof meta.id).toEqual('string')\n      expect(action.type).toEqual('logux/subscribe')\n      return [{ type: 'OTHER:load' }]\n    }\n  })\n\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'FOO' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'BAR' },\n    { id: [2, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    3,\n    { channel: 'foo', type: 'logux/subscribe' },\n    { id: [3, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    4,\n    { channel: 'bar', type: 'logux/subscribe' },\n    { id: [4, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n\n  expect(app.log.actions()).toEqual([\n    { type: 'FOO' },\n    { type: 'BAR' },\n    { channel: 'foo', type: 'logux/subscribe' },\n    { channel: 'bar', type: 'logux/subscribe' },\n    { type: 'REFOO' },\n    { id: '1 10:1:uuid 0', type: 'logux/processed' },\n    { type: 'REBAR' },\n    { id: '2 10:1:uuid 0', type: 'logux/processed' },\n    { type: 'FOO:load' },\n    { id: '3 10:1:uuid 0', type: 'logux/processed' },\n    { type: 'OTHER:load' },\n    { id: '4 10:1:uuid 0', type: 'logux/processed' }\n  ])\n})\n\nit('process action exactly once with accessAndProcess callback', async () => {\n  let app = createServer()\n  app.log.keepActions()\n\n  app.type('FOO', {\n    async accessAndProcess(ctx) {\n      await ctx.sendBack({ type: 'REFOO' })\n    }\n  })\n  app.otherType({\n    async accessAndProcess(ctx, action) {\n      if (action.type === 'BAR') {\n        await ctx.sendBack({ type: 'REBAR' })\n      }\n    }\n  })\n\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    1,\n    { type: 'FOO' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'BAR' },\n    { id: [2, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n\n  expect(app.log.actions()).toEqual([\n    { type: 'FOO' },\n    { type: 'BAR' },\n    { type: 'REFOO' },\n    { id: '1 10:1:uuid 0', type: 'logux/processed' },\n    { type: 'REBAR' },\n    { id: '2 10:1:uuid 0', type: 'logux/processed' }\n  ])\n})\n\nit('denies access on 403 error', async () => {\n  let app = createServer()\n  app.log.keepActions()\n\n  let error404 = new ResponseError(404, '/a')\n  let error403 = new ResponseError(403, '/a')\n  let error = new Error('test')\n\n  let catched: Error[] = []\n  app.on('error', e => {\n    catched.push(e)\n  })\n\n  app.type('E404', {\n    accessAndProcess() {\n      throw error404\n    }\n  })\n  app.type('E403', {\n    accessAndProcess() {\n      throw error403\n    }\n  })\n  app.type('ERROR', {\n    async accessAndProcess() {\n      throw error\n    }\n  })\n\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'E404' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'E403' },\n    { id: [2, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'ERROR' },\n    { id: [3, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  expect(app.log.actions()).toEqual([\n    {\n      action: { type: 'E404' },\n      id: '1 10:1:uuid 0',\n      reason: 'error',\n      type: 'logux/undo'\n    },\n    {\n      action: { type: 'E403' },\n      id: '2 10:1:uuid 0',\n      reason: 'denied',\n      type: 'logux/undo'\n    },\n    {\n      action: { type: 'ERROR' },\n      id: '3 10:1:uuid 0',\n      reason: 'error',\n      type: 'logux/undo'\n    }\n  ])\n  expect(catched).toEqual([error404, error])\n})\n\nit('undoes action with notFound on 404 error', async () => {\n  let app = createServer()\n  app.log.keepActions()\n\n  let error500 = new ResponseError(500, '/a')\n  let error404 = new ResponseError(404, '/a')\n  let error403 = new ResponseError(403, '/a')\n  let error = new Error('test')\n\n  let catched: Error[] = []\n  app.on('error', e => {\n    catched.push(e)\n  })\n\n  app.channel('e500', {\n    accessAndLoad() {\n      throw error500\n    }\n  })\n  app.channel('e404', {\n    accessAndLoad() {\n      throw error404\n    }\n  })\n  app.channel('e403', {\n    accessAndLoad() {\n      throw error403\n    }\n  })\n  app.channel('error', {\n    accessAndLoad() {\n      throw error\n    }\n  })\n\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'e500', type: 'logux/subscribe' },\n    { id: [1, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'e404', type: 'logux/subscribe' },\n    { id: [2, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'e403', type: 'logux/subscribe' },\n    { id: [3, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'error', type: 'logux/subscribe' },\n    { id: [4, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  expect(app.log.actions()).toEqual([\n    { channel: 'e500', type: 'logux/subscribe' },\n    { channel: 'e404', type: 'logux/subscribe' },\n    { channel: 'e403', type: 'logux/subscribe' },\n    { channel: 'error', type: 'logux/subscribe' },\n    {\n      action: { channel: 'e500', type: 'logux/subscribe' },\n      id: '1 10:1:uuid 0',\n      reason: 'error',\n      type: 'logux/undo'\n    },\n    {\n      action: { channel: 'e404', type: 'logux/subscribe' },\n      id: '2 10:1:uuid 0',\n      reason: 'notFound',\n      type: 'logux/undo'\n    },\n    {\n      action: { channel: 'e403', type: 'logux/subscribe' },\n      id: '3 10:1:uuid 0',\n      reason: 'denied',\n      type: 'logux/undo'\n    },\n    {\n      action: { channel: 'error', type: 'logux/subscribe' },\n      id: '4 10:1:uuid 0',\n      reason: 'error',\n      type: 'logux/undo'\n    }\n  ])\n  expect(catched).toEqual([error500, error])\n})\n\nit('allows to throws LoguxNotFoundError', async () => {\n  let app = createServer()\n  app.log.keepActions()\n\n  let catched: Error[] = []\n  app.on('error', e => {\n    catched.push(e)\n  })\n\n  app.channel('notFound', {\n    accessAndLoad() {\n      throw new LoguxNotFoundError()\n    }\n  })\n\n  let client = await connectClient(app, '10:1:uuid')\n  await sendTo(client, [\n    'sync',\n    2,\n    { channel: 'notFound', type: 'logux/subscribe' },\n    { id: [2, '10:1:uuid', 0], time: 1 }\n  ])\n  await setTimeout(100)\n  expect(app.log.actions()).toEqual([\n    { channel: 'notFound', type: 'logux/subscribe' },\n    {\n      action: { channel: 'notFound', type: 'logux/subscribe' },\n      id: '2 10:1:uuid 0',\n      reason: 'notFound',\n      type: 'logux/undo'\n    }\n  ])\n})\n\nit('undoes all other actions in a queue if error in one action occurs', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  let errors: string[] = []\n  app.on('error', e => {\n    errors.push(e.message)\n  })\n  app.type(\n    'GOOD 0',\n    {\n      access: () => true,\n      process() {\n        calls.push('GOOD 0')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'BAD',\n    {\n      access: () => true,\n      async process() {\n        await setTimeout(50)\n        calls.push('BAD')\n        throw new Error('BAD')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'GOOD 1',\n    {\n      access: () => true,\n      process() {\n        calls.push('GOOD 1')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'GOOD 2',\n    {\n      access: () => true,\n      process() {\n        calls.push('GOOD 2')\n      }\n    },\n    { queue: '1' }\n  )\n\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    3,\n    { type: 'GOOD 0' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'BAD' },\n    { id: [2, '10:client:other', 0], time: 1 },\n    { type: 'GOOD 1' },\n    { id: [3, '10:client:other', 0], time: 1 },\n    { type: 'GOOD 2' },\n    { id: [4, '10:client:other', 0], time: 1 }\n  ])\n  await setTimeout(50)\n\n  expect(errors).toEqual(['BAD'])\n  expect(calls).toEqual(['GOOD 0', 'BAD'])\n})\n\nit('does not add action with same ID to the queue', async () => {\n  let app = createServer()\n  let errors: string[] = []\n  let calls: string[] = []\n  app.on('error', e => {\n    errors.push(e.message)\n  })\n  app.type(\n    'FOO',\n    {\n      access: () => true,\n      process: () => {\n        calls.push('FOO')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'BAR',\n    {\n      access: () => true,\n      process: () => {\n        calls.push('BAR')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'BAZ',\n    {\n      access: () => true,\n      process: () => {\n        calls.push('BAZ')\n      }\n    },\n    { queue: '2' }\n  )\n  app.type(\n    'BOM',\n    {\n      access: () => true,\n      process: () => {\n        calls.push('BOM')\n      }\n    },\n    { queue: '2' }\n  )\n\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    4,\n    { type: 'FOO' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'BAR' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'BAZ' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'BOM' },\n    { id: [2, '10:client:other', 0], time: 1 }\n  ])\n\n  expect(errors).toEqual([])\n  expect(calls).toEqual(['FOO', 'BOM'])\n})\n\nit('does not undo actions in one queue if error occurs in another queue', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  let errors: string[] = []\n  app.on('error', e => {\n    errors.push(e.message)\n  })\n  app.type(\n    'BAD',\n    {\n      access: () => true,\n      process() {\n        calls.push('BAD')\n        throw new Error('BAD')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'GOOD 1',\n    {\n      access: () => true,\n      async process() {\n        await setTimeout(30)\n        calls.push('GOOD 1')\n      }\n    },\n    { queue: '2' }\n  )\n  app.type(\n    'GOOD 2',\n    {\n      access: () => true,\n      process() {\n        calls.push('GOOD 2')\n      }\n    },\n    { queue: '2' }\n  )\n\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    3,\n    { type: 'BAD' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'GOOD 1' },\n    { id: [2, '10:client:other', 0], time: 1 },\n    { type: 'GOOD 2' },\n    { id: [3, '10:client:other', 0], time: 1 }\n  ])\n  await setTimeout(50)\n\n  expect(errors).toEqual(['BAD'])\n  expect(calls).toEqual(['BAD', 'GOOD 1', 'GOOD 2'])\n})\n\nit('calls access, resend and process in a queue', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  app.type('FOO', {\n    async access() {\n      await setTimeout(50)\n      calls.push('FOO ACCESS')\n      return true\n    },\n    async process() {\n      await setTimeout(50)\n      calls.push('FOO PROCESS')\n    },\n    async resend() {\n      await setTimeout(50)\n      calls.push('FOO RESEND')\n      return ''\n    }\n  })\n  app.type('BAR', {\n    async access() {\n      calls.push('BAR ACCESS')\n      return true\n    },\n    async process() {\n      calls.push('BAR PROCESS')\n    },\n    async resend() {\n      calls.push('BAR RESEND')\n      return ''\n    }\n  })\n\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    2,\n    { type: 'FOO' },\n    { id: [1, '10:client:other', 0], time: 1 },\n    { type: 'BAR' },\n    { id: [2, '10:client:other', 0], time: 1 }\n  ])\n  await setTimeout(200)\n\n  expect(calls).toEqual([\n    'FOO ACCESS',\n    'FOO RESEND',\n    'FOO PROCESS',\n    'BAR ACCESS',\n    'BAR RESEND',\n    'BAR PROCESS'\n  ])\n})\n\nit('undoes all other actions in a queue if some action should be undone', async () => {\n  let test = createReporter()\n  test.app.type('FOO', {\n    access: () => false\n  })\n  test.app.type('BAR', {\n    access: () => true\n  })\n\n  let client = await connectClient(test.app)\n  await sendTo(client, [\n    'sync',\n    3,\n    { type: 'FOO' },\n    { id: [1, '10:uuid', 0], time: 1 },\n    { type: 'BAR' },\n    { id: [2, '10:uuid', 0], time: 1 }\n  ])\n\n  expect(test.names).toEqual([\n    'connect',\n    'authenticated',\n    'denied',\n    'add',\n    'add'\n  ])\n  expect(test.app.log.actions()).toEqual([\n    {\n      action: { type: 'FOO' },\n      id: '1 10:uuid 0',\n      reason: 'denied',\n      type: 'logux/undo'\n    },\n    {\n      action: { type: 'BAR' },\n      id: '2 10:uuid 0',\n      reason: 'error',\n      type: 'logux/undo'\n    }\n  ])\n})\n\nit('all actions are processed before destroy', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  app.type(\n    'queue 1 task 1',\n    {\n      async access() {\n        return true\n      },\n      async process() {\n        await setTimeout(30)\n        calls.push('queue 1 task 1')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'queue 1 task 2',\n    {\n      async access() {\n        return true\n      },\n      async process() {\n        await setTimeout(30)\n        calls.push('queue 1 task 2')\n      }\n    },\n    { queue: '1' }\n  )\n  app.type(\n    'queue 2 task 1',\n    {\n      async access() {\n        await setTimeout(50)\n        return true\n      },\n      async process() {\n        calls.push('queue 2 task 1')\n      }\n    },\n    { queue: '2' }\n  )\n  app.type(\n    'queue 2 task 2',\n    {\n      async access() {\n        return true\n      },\n      async process() {\n        calls.push('queue 2 task 2')\n      }\n    },\n    { queue: '2' }\n  )\n  app.type('during destroy', {\n    async access() {\n      return true\n    },\n    async process() {\n      calls.push('during destroy')\n    }\n  })\n\n  let client = await connectClient(app, '10:client:uuid')\n  sendTo(client, [\n    'sync',\n    4,\n    { type: 'queue 1 task 1' },\n    { id: [1, client.nodeId!, 0], time: 1 },\n    { type: 'queue 1 task 2' },\n    { id: [2, client.nodeId!, 0], time: 1 },\n    { type: 'queue 2 task 1' },\n    { id: [3, client.nodeId!, 0], time: 1 },\n    { type: 'queue 2 task 2' },\n    { id: [4, client.nodeId!, 0], time: 1 }\n  ])\n  await setTimeout(10)\n  await app.destroy()\n\n  expect(calls).toEqual([\n    'queue 1 task 1',\n    'queue 2 task 1',\n    'queue 2 task 2',\n    'queue 1 task 2'\n  ])\n})\n\nit('recognizes channel regex', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  app.channel(/ba./, {\n    access: () => true,\n    load: (_, action) => {\n      calls.push(action.channel)\n    }\n  })\n\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    3,\n    { channel: 'bar', type: 'logux/subscribe' },\n    { id: [1, client.nodeId || '', 0], time: 1 },\n    { channel: 'baz', type: 'logux/subscribe' },\n    { id: [2, client.nodeId || '', 0], time: 1 },\n    { channel: 'bom', type: 'logux/subscribe' },\n    { id: [3, client.nodeId || '', 0], time: 1 }\n  ])\n\n  expect(calls).toEqual(['bar', 'baz'])\n})\n\nit('recognizes channel pattern', async () => {\n  let app = createServer()\n  let calls: string[] = []\n  app.channel('/api/users/:id', {\n    access: () => true,\n    load: (_, action) => {\n      calls.push(action.channel)\n    }\n  })\n\n  let client = await connectClient(app, '10:client:uuid')\n  await sendTo(client, [\n    'sync',\n    3,\n    { channel: '/api/users/5', type: 'logux/subscribe' },\n    { id: [1, client.nodeId || '', 0], time: 1 },\n    { channel: '/api/users/10', type: 'logux/subscribe' },\n    { id: [2, client.nodeId || '', 0], time: 1 },\n    { channel: '/api/users/10/9/8', type: 'logux/subscribe' },\n    { id: [3, client.nodeId || '', 0], time: 1 }\n  ])\n\n  expect(calls).toEqual(['/api/users/5', '/api/users/10'])\n})\n\nit('removes empty queues', async () => {\n  let app = createServer()\n  app.type('FOO', {\n    access: () => true,\n    process: async () => {\n      await setTimeout(50)\n    }\n  })\n  app.type('BAR', {\n    access: () => true\n  })\n\n  let client = await connectClient(app, '10:client:uuid')\n  sendTo(client, [\n    'sync',\n    2,\n    { type: 'FOO' },\n    { id: [1, '10:client:uuid', 0], time: 1 },\n    { type: 'BAR' },\n    { id: [2, '10:client:uuid', 0], time: 1 }\n  ])\n\n  await setTimeout(10)\n  expect(privateMethods(app).queues.size).toEqual(1)\n  await setTimeout(50)\n  expect(privateMethods(app).queues.size).toEqual(0)\n})\n\nit('replaces Node class if necessary', async () => {\n  class OtherNode extends FilteredNode {\n    syncSinceQuery(): { added: number; entries: [Action, Meta][] } {\n      return {\n        added: 0,\n        entries: [\n          [\n            { type: 'FOO' },\n            { added: 0, id: '1 server:uuid 0', reasons: [], time: 1 }\n          ]\n        ]\n      }\n    }\n  }\n  let app = createServer({\n    Node: OtherNode\n  })\n\n  let client = await connectClient(app, '10:client:uuid')\n  await setTimeout(10)\n  expect(actions(client)).toEqual([{ type: 'FOO' }])\n})\n\nit('allows to change how server loads initial actions', async () => {\n  let app = createServer({})\n  app.sendOnConnect(async (ctx, lastSync) => {\n    expect(ctx.clientId).toEqual('10:client')\n    expect(ctx.subprotocol).toEqual(1)\n    expect(lastSync).toEqual(0)\n    return [\n      [\n        { type: 'FOO' },\n        { added: 0, id: '1 server:uuid 0', reasons: [], server: '', time: 2 }\n      ],\n      [\n        { type: 'BAR' },\n        { added: 0, id: '1 server:uuid 0', reasons: [], server: '', time: 1 }\n      ]\n    ]\n  })\n  let client = await connectClient(app, '10:client:uuid')\n  await setTimeout(10)\n  expect(actions(client)).toEqual([{ type: 'BAR' }, { type: 'FOO' }])\n})\n"
  },
  {
    "path": "test/fixtures/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIID7jCCAtagAwIBAgIUO6cuMpNPGoG1mUO5WcSQMzvNaHkwDQYJKoZIhvcNAQEL\nBQAwZjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk5ZMREwDwYDVQQHEwhOZXcgWW9y\nazEOMAwGA1UEChMFTG9ndXgxEzARBgNVBAsTCk9wZXJhdGlvbnMxEjAQBgNVBAMT\nCWxvZ3V4Lm9yZzAeFw0xODA1MTAyMDQ0MDBaFw0yMzA1MDkyMDQ0MDBaMEwxCzAJ\nBgNVBAYTAlJVMQ8wDQYDVQQIEwZNb3Njb3cxDDAKBgNVBAcTA01TSzEeMBwGA1UE\nAxMVbG9jYWxob3N0LmFtcGxpZnIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzD7FPEH7miQ8CZDPI2OWgPF5CXzqojns1bPkEhhEPrGEBtRkYETh\nFpa0+Z1XD61t++t2yoAY1V09V7yV8EGyFZzeVL3IRiDUm0IE8wiuhvG5pCRwOxL2\nW2d8+jU4Xu7lmo2IGuZbQyCc80NcNS+fnY8uH7aYPKN09KaJQ2l5LnE3czncB6CW\nLiNA3rbQKgufjgODl1NsNmgS7yNhFwMJcl09mpdEY/wDpXshLYd0phAi0rz0ypcJ\nUlaCvoU7dRT3k/8jBKa6hZHSxnaXercwdb/bHhBm8qpDRfcr1sGeM9rxwi4fgp8L\nU/pdNQ1ALfDqCGv8sOCVEZsaTgrowRVxIwIDAQABo4GtMIGqMA4GA1UdDwEB/wQE\nAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW\nBBT15Q7OhWASNHp2ZXD0MEVm6NKurzAfBgNVHSMEGDAWgBT1qRLU7Q0DOb2ewaOL\nP0ZpcRS8SzA1BgNVHREELjAsghVsb2NhbGhvc3QuYW1wbGlmci5jb22CDSouYW1w\nbGlmci5jb22HBH8AAAEwDQYJKoZIhvcNAQELBQADggEBALKi2D2BDIYIeqlfctnx\nPKTbwZCeWOLjCdD8sgcLXOL6QVidWHOnlfU5J2mtoThcbAr8eJ+DXRm4ps/pLSkG\nEsd1H6Ki/M3EPzAm1aALkPeBram3TFKpBLaZproIVgKiL1WQeCqCrn7ZAck1A/Lj\nCVVtn8hC02SA5JpMs39PYCa7X9F5bGeYbGJqh0z87HuG6Tfae/B9LHPiormCUj6N\nBvR10juL1f6ai7dUjTz704OmrFHqCMo+Bdf+pzTsLZqmmSB4B//bmh/mFMuTfm1X\nocHvKwcTf4EYdHZroh0/kuTMXgww+cujNND6W70lYfOkfIJ8T22kwb/uqCPtgRRd\n1i8=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/fixtures/key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAzD7FPEH7miQ8CZDPI2OWgPF5CXzqojns1bPkEhhEPrGEBtRk\nYEThFpa0+Z1XD61t++t2yoAY1V09V7yV8EGyFZzeVL3IRiDUm0IE8wiuhvG5pCRw\nOxL2W2d8+jU4Xu7lmo2IGuZbQyCc80NcNS+fnY8uH7aYPKN09KaJQ2l5LnE3cznc\nB6CWLiNA3rbQKgufjgODl1NsNmgS7yNhFwMJcl09mpdEY/wDpXshLYd0phAi0rz0\nypcJUlaCvoU7dRT3k/8jBKa6hZHSxnaXercwdb/bHhBm8qpDRfcr1sGeM9rxwi4f\ngp8LU/pdNQ1ALfDqCGv8sOCVEZsaTgrowRVxIwIDAQABAoIBAQCkM5K97w4nzhm2\nVwUwnk/ROlDkn9jCs28EH6usIHY9MNnD490OyFFtp5u3Uhc8M2HItnS6OGG+p0c5\n0hN5JFfXqFXWKv1n490JNPplqQUm2A83N1RDKeuFcJ25SjAXolhU+JQDjE6ymPWV\nXQI0gCUCtqmONW4O0hqk1X5lA9a4zjuZkDWCUNuuLR/udoEJmgf26Hf8uLlvRiOD\nRCOzOTN+xHzUJhFMGx7VsL9cBxyKL55VnXfXAxJUwbUSJmMFuEz+1FEkXYiudYfn\n6mPbicmM36JJkZCbwT4Z9Icx62WO9vR7PocFParb/iiBG1cEBbNdAfBDJUrkHiOT\nWs3nj0uBAoGBANIciSOFlPePzkJ3InogJG1A4kyYeIAjjh4dZssHCYjHj9/MRfkD\nzQ+QYd0Jw3/yxw8+oNjxx6Fd7D5opTB6SfD6b74dW8DRSC5FkvJ0cums1CrTXNUw\nmhEDrR8UlgmsNNGHspyspWm2BCFUwkPY5aDYwo0B3HzoWf4AzipwnUYFAoGBAPja\nP3UETswZ+YCQXK0bkv7o/2crZajKAq3L3nJ/x98VWIsqHX4hOxsGy+PBx+EGkHqP\nO4dDQwntoc3DH3RCajm380qB2QWeN88eJb6mbIV9O/+ysyGXX+kM8kZpJQN+Ql4t\nVGWCYZHxuPPQakaT397mywiTKGinneVBCjSzrhsHAoGAHIovvpl4gKAR/kk8b6ZK\nDGUR2CGlzJIHzeNkgRN1ohSpYFbY8lgn1INiJ6oZ2hlaHKH/Kzi8Sxj87AU+2vTh\nclAyOXq3aduDmHVu3mwe58rIDwEizPqLEuCS9XPQZYP0sLlj85An79H/gZ+Hu7uM\nhWqsEoc8MeNFxhDJ8E3XrxkCgYAWSQ03wHwCAS172vHBut9uHpWIurUu6XBV+hTg\nshrHGpVEWTAs9HLjl7c4nUj2GO1lXGBbW6WsRPChiaDOe4ghxRxvhrNVsnaTAMMm\nkKbVSYLPAkTSdEjtiPBFZ/Mdnff5kRumv4dXV4tVoktyKJn6zzZNfUg4HxKfzjRI\nxfKIjwKBgCVZHfS0UL9gM0wROa7axzifRuUNn2UzESmfnZlyMOhiTWQ2zb+avRQ4\ndK16NACGe+ZbYivra+wUIv7x/iSsi37Rz7rl8WFC9Q9GiylLqeUlezedekTsrdwB\nVVVEAxq6meQEY6dcgPOnwamfm86diHCeAesmCtB4AJAQfkKm59Ku\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "test/force-colors.js",
    "content": "process.env.FORCE_COLOR = '1'\n"
  },
  {
    "path": "test/servers/autoload-error-modules.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  root: import.meta.dirname,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen().then(async () => {\n  await app.autoloadModules('error-modules/*/index.js')\n})\n"
  },
  {
    "path": "test/servers/autoload-modules.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  root: import.meta.dirname,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.autoloadModules().then(async () => {\n  await app.listen()\n})\n"
  },
  {
    "path": "test/servers/destroy.js",
    "content": "#!/usr/bin/env node\n\nimport { setTimeout } from 'node:timers/promises'\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  port: 2000,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\napp.unbind.push(async () => {\n  await setTimeout(10)\n  app.logger.info('Custom destroy task finished')\n})\n\nawait app.listen()\n\nprocess.on('message', async msg => {\n  if (msg === 'close') {\n    console.error('close')\n    await app.destroy()\n  }\n})\n"
  },
  {
    "path": "test/servers/eacces.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  port: 1000,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/eaddrinuse.js",
    "content": "#!/usr/bin/env node\n\nimport os from 'node:os'\n\nimport { Server } from '../../index.js'\n\nos.platform = () => 'linux'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  port: 2001,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/error-modules/wrond-export/index.js",
    "content": "export default 'wrong module export'\n"
  },
  {
    "path": "test/servers/json.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  logger: 'json',\n  minSubprotocol: 1,\n  port: 2000,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/logger.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    host: '127.0.0.1',\n    minSubprotocol: 1,\n    subprotocol: 1\n  })\n)\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\napp.logger.info({ field: 1 }, 'Hi from custom logger')\napp.logger.debug('Debug message')\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/missed.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    host: '127.0.0.1',\n    subprotocol: 1\n  })\n)\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/modules/child/index.foo.js",
    "content": "setTimeout(() => {\n  let error = new Error('Test Error')\n  error.stack = `${error.stack.split('\\n')[0]}\\nfake stacktrace`\n  throw error\n}, 10)\n"
  },
  {
    "path": "test/servers/modules/child/index.js",
    "content": "import { setTimeout } from 'node:timers/promises'\n\nexport default async server => {\n  await setTimeout(100)\n  console.log(`Child path module: ${server.options.subprotocol}`)\n}\n"
  },
  {
    "path": "test/servers/modules/child/lib/lib.js",
    "content": "setTimeout(() => {\n  let error = new Error('Test Error')\n  error.stack = `${error.stack.split('\\n')[0]}\\nfake stacktrace`\n  throw error\n}, 50)\n"
  },
  {
    "path": "test/servers/modules/root.js",
    "content": "export default server => {\n  console.log(`Root path module: ${server.options.subprotocol}`)\n}\n"
  },
  {
    "path": "test/servers/modules/root.test.js",
    "content": "throw new Error('No load')\n"
  },
  {
    "path": "test/servers/options.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    host: '127.0.0.1',\n    minSubprotocol: 1,\n    subprotocol: 1\n  })\n)\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/root.js",
    "content": "#!/usr/bin/env node\n\nimport { join } from 'node:path'\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    host: '127.0.0.1',\n    minSubprotocol: 1,\n    root: join(import.meta.dirname, '..', 'fixtures'),\n    subprotocol: 1\n  })\n)\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test/servers/throw.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.on('fatal', e => app.logger.info(`Fatal event: ${e.message}`))\n\nsetTimeout(() => {\n  let error = new Error('Test Error')\n  error.stack = `${error.stack.split('\\n')[0]}\\nfake stacktrace`\n  throw error\n}, 10)\n"
  },
  {
    "path": "test/servers/unbind.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\nawait app.destroy()\n\nsetTimeout(() => {}, 10000)\n"
  },
  {
    "path": "test/servers/uncatch.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  subprotocol: 1\n})\napp.nodeId = 'server:FnXaqDxY'\n\napp.on('fatal', e => app.logger.info(`Fatal event: ${e.message}`))\n\nawait new Promise((resolve, reject) => {\n  setTimeout(() => {\n    let error = new Error('Test Error')\n    error.stack = `${error.stack.split('\\n')[0]}\\nfake stacktrace`\n    reject(error)\n  }, 50)\n})\n"
  },
  {
    "path": "test/servers/unknown.js",
    "content": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    host: '127.0.0.1',\n    maxSubprotocol: 1,\n    subprotocol: 1\n  })\n)\napp.nodeId = 'server:FnXaqDxY'\n\napp.auth(async () => true)\n\nawait app.listen()\n"
  },
  {
    "path": "test-client/index.d.ts",
    "content": "import type {\n  LoguxSubscribeAction,\n  LoguxUnsubscribeAction\n} from '@logux/actions'\nimport type {\n  Action,\n  AnyAction,\n  ClientNode,\n  TestLog,\n  TestPair\n} from '@logux/core'\n\nimport type { ServerMeta } from '../base-server/index.js'\nimport type { TestServer } from '../test-server/index.js'\n\nexport class LoguxActionError extends Error {\n  action: Action\n}\n\nexport interface TestClientOptions {\n  cookie?: object\n  headers?: object\n  httpHeaders?: { [key: string]: string }\n  subprotocol?: number\n  token?: string\n}\n\n/**\n * Client to test server.\n *\n * ```js\n * import { TestServer } from '@logux/server'\n * import postsModule from './posts.js'\n * import authModule from './auth.js'\n *\n * let destroyable\n * afterEach(() => {\n *   if (destroyable) destroyable.destroy()\n * })\n *\n * function createServer () {\n *   destroyable = new TestServer()\n *   return destroyable\n * }\n *\n * it('check auth', () => {\n *   let server = createServer()\n *   authModule(server)\n *   await server.connect('1', { token: 'good' })\n *    expect(() => {\n *      await server.connect('2', { token: 'bad' })\n *    }).rejects.toEqual({\n *      error: 'Wrong credentials'\n *    })\n * })\n *\n * it('creates and loads posts', () => {\n *   let server = createServer()\n *   postsModule(server)\n *   let client1 = await server.connect('1')\n *   await client1.process({ type: 'posts/add', post })\n *   let client1 = await server.connect('2')\n *   expect(await client2.subscribe('posts')).toEqual([\n *     { type: 'posts/add', post }\n *   ])\n * })\n * ```\n */\nexport class TestClient {\n  /**\n   * Client’s ID.\n   *\n   * ```js\n   * let client = new TestClient(server, '10')\n   * client.clientId //=> '10:1'\n   * ```\n   */\n  clientId: string\n\n  /**\n   * Client’s log with extra methods to check actions inside.\n   *\n   * ```js\n   * console.log(client.log.entries())\n   * ```\n   */\n  log: TestLog\n\n  /**\n   * Logux node.\n   */\n  node: ClientNode<object, TestLog<ServerMeta>>\n\n  /**\n   * Client’s node ID.\n   *\n   * ```js\n   * let client = new TestClient(server, '10')\n   * client.nodeId //=> '10:1:1'\n   * ```\n   */\n  nodeId: string\n\n  /**\n   * Connection channel between client and server to track sent messages.\n   *\n   * ```js\n   * console.log(client.pair.leftSent)\n   * ```\n   */\n  pair: TestPair\n\n  /**\n   * User ID.\n   *\n   * ```js\n   * let client = new TestClient(server, '10')\n   * client.userId //=> '10'\n   * ```\n   */\n  userId: string\n\n  /**\n   * @param server Test server.\n   * @param userId User ID.\n   * @param opts Other options.\n   */\n  constructor(server: TestServer, userId: string, opts?: TestClientOptions)\n\n  /**\n   * Collect actions added by server and other clients during the `test` call.\n   *\n   * ```js\n   * let answers = await client.collect(async () => {\n   *   client.log.add({ type: 'pay' })\n   *   await delay(10)\n   * })\n   * expect(actions).toEqual([{ type: 'paid' }])\n   * ```\n   *\n   * @param test Function, where do you expect action will be received\n   * @returns Promise with all received actions\n   */\n  collect(test: () => Promise<unknown>): Promise<Action[]>\n\n  /**\n   * Connect to test server.\n   *\n   * ```js\n   * let client = new TestClient(server, '10')\n   * await client.connect()\n   * ```\n   *\n   * @params Connection credentials.\n   * @returns Promise until the authorization.\n   */\n  connect(opts?: { token: string }): Promise<void>\n\n  /**\n   * Disconnect from test server.\n   *\n   * ```js\n   * await client.disconnect()\n   * ```\n   *\n   * @returns Promise until connection close.\n   */\n  disconnect(): Promise<void>\n\n  /**\n   * Send action to the sever and collect all response actions.\n   *\n   * ```js\n   * await client.process({ type: 'posts/add', post })\n   * let posts = await client.subscribe('posts')\n   * expect(posts).toHaveLength(1)\n   * ```\n   *\n   * @param action New action.\n   * @param meta Optional action’s meta.\n   * @returns Promise until `logux/processed` answer.\n   */\n  process(action: AnyAction, meta?: Partial<ServerMeta>): Promise<Action[]>\n\n  /**\n   * Collect actions received from server during the `test` call.\n   *\n   * ```js\n   * let answers = await client1.received(async () => {\n   *   await client2.process({ type: 'resend' })\n   * })\n   * expect(actions).toEqual([{ type: 'local' }])\n   * ```\n   *\n   * @param test Function, where do you expect action will be received\n   * @returns Promise with all received actions\n   */\n  received(test: () => unknown): Promise<Action[]>\n\n  /**\n   * Subscribe to the channel and collect all actions during the subscription.\n   *\n   * ```js\n   * let posts = await client.subscribe('posts')\n   * expect(posts).toEqual([\n   *   { type: 'posts/add', post }\n   * ])\n   * ```\n   *\n   * @param channel Channel name or `logux/subscribe` action.\n   * @param filter Optional filter for subscription.\n   * @param since Optional time from last data.\n   * @returns Promise with all actions from the server.\n   */\n  subscribe(\n    channel: LoguxSubscribeAction | string,\n    filter?: object,\n    since?: { id: string; time: number }\n  ): Promise<Action[]>\n\n  /**\n   * Unsubscribe client from the channel.\n   *\n   * ```js\n   * await client.unsubscribe('posts')\n   * ```\n   *\n   * @param channel Channel name or `logux/subscribe` action.\n   * @param filter Optional filter for subscription.\n   * @returns Promise until server will remove client from subscribers.\n   */\n  unsubscribe(\n    channel: LoguxUnsubscribeAction | string,\n    filter?: object\n  ): Promise<Action[]>\n}\n"
  },
  {
    "path": "test-client/index.js",
    "content": "import { ClientNode, TestPair } from '@logux/core'\nimport cookie from 'cookie'\nimport { setTimeout } from 'node:timers/promises'\n\nimport { filterMeta } from '../filter-meta/index.js'\n\nexport class TestClient {\n  constructor(server, userId, opts = {}) {\n    this.server = server\n    this.pair = new TestPair()\n    let clientId = server.testUsers[userId] || 0\n    clientId += 1\n    server.testUsers[userId] = clientId\n    this.userId = userId\n    this.clientId = `${userId}:${clientId}`\n    this.nodeId = `${this.clientId}:1`\n    this.log = server.options.time.nextLog({ nodeId: this.nodeId })\n    this.node = new ClientNode(this.nodeId, this.log, this.pair.left, {\n      ...opts,\n      fixTime: false,\n      onSend(action, meta) {\n        return [action, filterMeta(meta)]\n      }\n    })\n    this.pair.right.ws = {\n      _socket: {\n        remoteAddress: '127.0.0.1'\n      },\n      upgradeReq: {\n        headers: opts.httpHeaders || {}\n      }\n    }\n    if (opts.headers) {\n      this.node.setLocalHeaders(opts.headers)\n    }\n    if (opts.cookie) {\n      this.pair.right.ws.upgradeReq.headers.cookie = Object.keys(opts.cookie)\n        .map(i => cookie.serialize(i, opts.cookie[i]))\n        .join('; ')\n    }\n    server.unbind.push(() => {\n      this.node.destroy()\n    })\n  }\n\n  async collect(test) {\n    let added = []\n    let unbind = this.node.log.on('add', (action, meta) => {\n      if (!meta.id.includes(` ${this.nodeId} `)) {\n        added.push(action)\n      }\n    })\n    await test()\n    unbind()\n    return added\n  }\n\n  connect() {\n    return new Promise((resolve, reject) => {\n      this.node.throwsError = false\n      let unbind = this.node.on('error', e => {\n        if (e.name === 'LoguxError' && e.type === 'wrong-credentials') {\n          reject(new Error('Wrong credentials'))\n        } else {\n          reject(e)\n        }\n      })\n\n      this.server.addClient(this.pair.right)\n      void this.node.connection.connect()\n      void this.node.waitFor('synchronized').then(() => {\n        this.node.throwsError = true\n        unbind()\n        resolve()\n      })\n    })\n  }\n\n  disconnect() {\n    this.node.connection.disconnect()\n    return this.pair.wait('right')\n  }\n\n  process(action, meta) {\n    return this.collect(async () => {\n      return new Promise((resolve, reject) => {\n        let id\n        let lastError\n        let unbindError = this.server.on('error', e => {\n          lastError = e\n        })\n        let unbindProcessed = this.log.type('logux/processed', other => {\n          if (other.id === id) {\n            unbindProcessed()\n            unbindUndo()\n            unbindError()\n            resolve()\n          }\n        })\n        let unbindUndo = this.log.type('logux/undo', other => {\n          if (other.id === id) {\n            unbindProcessed()\n            unbindUndo()\n            unbindError()\n            let error\n            if (other.reason === 'denied') {\n              error = new Error('Action was denied')\n            } else if (other.reason === 'unknownType') {\n              error = new Error(\n                `Server does not have callbacks for ${action.type} actions`\n              )\n            } else if (other.reason === 'wrongChannel') {\n              error = new Error(\n                `Server does not have callbacks for ${action.channel} channel`\n              )\n            } else if (lastError) {\n              error = lastError\n            } else {\n              error = new Error('Server undid action')\n            }\n            error.action = other\n            reject(error)\n          }\n        })\n        this.log.add(action, meta).then(newMeta => {\n          if (newMeta) {\n            id = newMeta.id\n          } else {\n            reject(new Error(`Action ${meta.id} was already in log`))\n          }\n        })\n      })\n    })\n  }\n\n  async received(test) {\n    let actions = []\n    let unbind = this.log.on('add', (action, meta) => {\n      if (!meta.id.includes(` ${this.nodeId} `)) {\n        actions.push(action)\n      }\n    })\n    await test()\n    await setTimeout(1)\n    unbind()\n    return actions\n  }\n\n  async subscribe(channel, filter, since) {\n    let action = channel\n    if (typeof channel === 'string') {\n      action = { channel, type: 'logux/subscribe' }\n    }\n    if (filter) {\n      action.filter = filter\n    }\n    if (since) {\n      action.since = since\n    }\n    let actions = await this.process(action)\n    return actions.filter(i => i.type !== 'logux/processed')\n  }\n\n  unsubscribe(channel, filter) {\n    let action = channel\n    if (typeof channel === 'string') {\n      action = { channel, type: 'logux/unsubscribe' }\n    }\n    if (filter) {\n      action.filter = filter\n    }\n    return this.process(action)\n  }\n}\n"
  },
  {
    "path": "test-client/index.test.ts",
    "content": "import { TestTime } from '@logux/core'\nimport { restoreAll, type Spy, spyOn } from 'nanospy'\nimport { setTimeout } from 'node:timers/promises'\nimport { afterEach, expect, it } from 'vitest'\n\nimport { type LoguxActionError, TestClient, TestServer } from '../index.js'\n\nlet server: TestServer\nafterEach(() => {\n  restoreAll()\n  server.destroy()\n})\n\nasync function catchError(cb: () => Promise<any>): Promise<LoguxActionError> {\n  let err: LoguxActionError | undefined\n  try {\n    await cb()\n  } catch (e) {\n    err = e as LoguxActionError\n  }\n  if (!err) throw new Error('Error was no thrown')\n  return err\n}\n\nfunction privateMethods(obj: object): any {\n  return obj\n}\n\nit('connects and disconnect', async () => {\n  server = new TestServer()\n  let client1 = new TestClient(server, '10')\n  let client2 = new TestClient(server, '10')\n  expect(client1.nodeId).toEqual('10:1:1')\n  expect(client1.clientId).toEqual('10:1')\n  expect(client1.userId).toEqual('10')\n  expect(client2.nodeId).toEqual('10:2:1')\n  await Promise.all([client1.connect(), client2.connect()])\n  expect(Array.from(server.clientIds.keys())).toEqual(['10:1', '10:2'])\n  await client1.disconnect()\n  expect(Array.from(server.clientIds.keys())).toEqual(['10:2'])\n})\n\nit('sends and collect actions', async () => {\n  server = new TestServer()\n  server.type('FOO', {\n    access: () => true,\n    process(ctx) {\n      ctx.sendBack({ type: 'BAR' })\n    }\n  })\n  server.type('RESEND', {\n    access: () => true,\n    resend: () => ({ user: '10' })\n  })\n  let [client1, client2] = await Promise.all([\n    server.connect('10'),\n    server.connect('11')\n  ])\n  client1.log.keepActions()\n  let received = await client1.collect(async () => {\n    await client1.log.add({ type: 'FOO' })\n    await setTimeout(10)\n    await client2.log.add({ type: 'RESEND' })\n    await setTimeout(10)\n  })\n  expect(received).toEqual([\n    { type: 'BAR' },\n    { id: '1 10:1:1 0', type: 'logux/processed' },\n    { type: 'RESEND' }\n  ])\n  expect(client1.log.actions()).toEqual([\n    { type: 'FOO' },\n    { type: 'BAR' },\n    { id: '1 10:1:1 0', type: 'logux/processed' },\n    { type: 'RESEND' }\n  ])\n})\n\nit('allows to change time', () => {\n  let time = new TestTime()\n  let server1 = new TestServer({ time })\n  let server2 = new TestServer({ time })\n  expect(server1.options.time).toBe(time)\n  expect(server2.options.time).toBe(time)\n  expect(server1.nodeId).not.toEqual(server2.nodeId)\n})\n\nit('tracks action processing', async () => {\n  server = new TestServer()\n  server.type('FOO', {\n    access: () => true\n  })\n  server.type('ERR', {\n    access: () => true,\n    process() {\n      throw new Error('test')\n    }\n  })\n  server.type('DENIED', {\n    access: () => false\n  })\n  server.type('UNDO', {\n    access: () => true,\n    process(ctx, action, meta) {\n      server.undo(action, meta)\n    }\n  })\n  let client = await server.connect('10')\n\n  let processed = await client.process({ type: 'FOO' })\n  expect(processed).toEqual([{ id: '1 10:1:1 0', type: 'logux/processed' }])\n\n  let notDenied = await catchError(async () => {\n    await server.expectDenied(() => client.process({ type: 'FOO' }))\n  })\n  expect(notDenied.message).toEqual('Actions passed without error')\n\n  let serverError = await catchError(() => client.process({ type: 'ERR' }))\n  expect(serverError.message).toEqual('test')\n  expect(serverError.action).toEqual({\n    action: { type: 'ERR' },\n    id: '5 10:1:1 0',\n    reason: 'error',\n    type: 'logux/undo'\n  })\n\n  let accessError = await catchError(() => client.process({ type: 'DENIED' }))\n  expect(accessError.message).toEqual('Action was denied')\n\n  await server.expectDenied(() => client.process({ type: 'DENIED' }))\n\n  let unknownError = await catchError(() => client.process({ type: 'UNKNOWN' }))\n  expect(unknownError.message).toEqual(\n    'Server does not have callbacks for UNKNOWN actions'\n  )\n\n  let customError1 = await catchError(() => client.process({ type: 'UNDO' }))\n  expect(customError1.message).toEqual('Server undid action')\n\n  let customError2 = await catchError(async () => {\n    await server.expectDenied(() => client.process({ type: 'UNDO' }))\n  })\n  expect(customError2.message).toEqual('Undo was with error reason, not denied')\n\n  await server.expectUndo('error', () => client.process({ type: 'UNDO' }))\n\n  let reasonError = await catchError(async () => {\n    await server.expectUndo('another', () => client.process({ type: 'UNDO' }))\n  })\n  expect(reasonError.message).toEqual('Undo was with error reason, not another')\n\n  let noReasonError = await catchError(async () => {\n    await server.expectUndo('error', () => client.process({ type: 'UNKNOWN' }))\n  })\n  expect(noReasonError.message).toEqual(\n    'Server does not have callbacks for UNKNOWN actions'\n  )\n\n  await server.expectError('test', async () => {\n    await client.process({ type: 'ERR' })\n  })\n  await server.expectError(/te/, async () => {\n    await client.process({ type: 'ERR' })\n  })\n  let wrongMessageError = await catchError(async () => {\n    await server.expectError('te', async () => {\n      await client.process({ type: 'ERR' })\n    })\n  })\n  expect(wrongMessageError.message).toEqual('test')\n  let noErrorError = await catchError(async () => {\n    await server.expectError('te', async () => {\n      await client.process({ type: 'FOO' })\n    })\n  })\n  expect(noErrorError.message).toEqual('Actions passed without error')\n})\n\nit('detects action ID duplicate', async () => {\n  server = new TestServer()\n  server.type('FOO', {\n    access: () => true\n  })\n  let client = await server.connect('10')\n  client.log.keepActions()\n\n  let processed = await client.process({ type: 'FOO' }, { id: '1 10:1:1 0' })\n  expect(processed).toEqual([{ id: '1 10:1:1 0', type: 'logux/processed' }])\n\n  let err = await catchError(async () => {\n    await client.process({ type: 'FOO' }, { id: '1 10:1:1 0' })\n  })\n  expect(err.message).toEqual('Action 1 10:1:1 0 was already in log')\n})\n\nit('tracks subscriptions', async () => {\n  server = new TestServer()\n  server.channel<object, object>('foo', {\n    access: () => true,\n    load(ctx, action) {\n      ctx.sendBack({ a: action.filter?.a, since: action.since, type: 'FOO' })\n    }\n  })\n  let client = await server.connect('10')\n  let actions1 = await client.subscribe('foo')\n  expect(actions1).toEqual([{ a: undefined, type: 'FOO' }])\n\n  await client.unsubscribe('foo')\n  expect(privateMethods(server).subscribers).toEqual({})\n\n  let actions2 = await client.subscribe('foo', { a: 1 })\n  expect(actions2).toEqual([{ a: 1, type: 'FOO' }])\n\n  let actions3 = await client.subscribe('foo', undefined, {\n    id: '1 1:0:0',\n    time: 1\n  })\n  expect(actions3).toEqual([{ since: { id: '1 1:0:0', time: 1 }, type: 'FOO' }])\n\n  await client.unsubscribe('foo', { a: 1 })\n  expect(privateMethods(server).subscribers).toEqual({\n    foo: {\n      '10:1:1': {\n        filters: {\n          '{}': true\n        }\n      }\n    }\n  })\n\n  await client.unsubscribe('foo')\n  expect(privateMethods(server).subscribers).toEqual({})\n\n  let actions4 = await client.subscribe({\n    channel: 'foo',\n    filter: { a: 2 },\n    type: 'logux/subscribe'\n  })\n  expect(actions4).toEqual([{ a: 2, type: 'FOO' }])\n\n  let unknownError = await catchError(() => client.subscribe('unknown'))\n  expect(unknownError.message).toEqual(\n    'Server does not have callbacks for unknown channel'\n  )\n})\n\nit('prints server log', async () => {\n  let reporterStream = {\n    write() {}\n  }\n  spyOn(reporterStream, 'write', () => {})\n  server = new TestServer({\n    logger: { stream: reporterStream }\n  })\n  await server.connect('10:uuid')\n  expect((reporterStream.write as any as Spy).callCount).toEqual(2)\n})\n\nit('tests authentication', async () => {\n  server = new TestServer()\n  server.options.minSubprotocol = 1\n  server.auth(({ token, userId }) => userId === '10' && token === 'good')\n\n  let wrong = await catchError(async () => {\n    await server.connect('10', { subprotocol: 1, token: 'bad' })\n  })\n  expect(wrong.message).toEqual('Wrong credentials')\n\n  await server.expectWrongCredentials('10', { subprotocol: 1, token: 'bad' })\n\n  let error1 = await catchError(async () => {\n    await server.connect('10', { subprotocol: 0 })\n  })\n  expect(error1.message).toContain('wrong-subprotocol')\n\n  await server.connect('10', { subprotocol: 1, token: 'good' })\n\n  let notWrong = await catchError(async () => {\n    await server.expectWrongCredentials('10', { subprotocol: 1, token: 'good' })\n  })\n  expect(notWrong.message).toEqual('Credentials passed')\n})\n\nit('disables build-in auth', () => {\n  server = new TestServer({ auth: false })\n  expect(privateMethods(server).authenticator).not.toBeDefined()\n})\n\nit('sets client headers', async () => {\n  server = new TestServer()\n  await server.connect('10', { headers: { locale: 'fr' } })\n  let node = server.clientIds.get('10:1')?.node\n  expect(node?.remoteHeaders).toEqual({ locale: 'fr' })\n})\n\nit('sets client cookie', async () => {\n  server = new TestServer()\n  server.auth(({ cookie }) => cookie.token === 'good')\n  await server.connect('10', { cookie: { token: 'good' } })\n  await server.expectWrongCredentials('10', { cookie: { token: 'bad' } })\n})\n\nit('sets custom HTTP headers', async () => {\n  server = new TestServer()\n  server.auth(({ client }) => client.httpHeaders.authorization === 'good')\n  await server.connect('10', { httpHeaders: { authorization: 'good' } })\n  await server.expectWrongCredentials('10', {\n    httpHeaders: { authorization: 'bad' }\n  })\n  await server.expectWrongCredentials('10')\n})\n\nit('collects received actions', async () => {\n  server = new TestServer()\n  server.type('foo', {\n    access: () => true,\n    process(ctx) {\n      ctx.sendBack({ type: 'bar' })\n    }\n  })\n  let client = await server.connect('10')\n  let actions = await client.received(async () => {\n    await client.process({ type: 'foo' })\n  })\n  expect(actions).toEqual([\n    { type: 'bar' },\n    { id: '1 10:1:1 0', type: 'logux/processed' }\n  ])\n})\n\nit('receives HTTP requests', async () => {\n  server = new TestServer()\n  server.http('GET', '/a', (req, res) => {\n    res.writeHead(200, { 'Content-Type': 'text/plain' })\n    res.end(String(req.headers['x-test'] ?? 'empty'))\n  })\n\n  let response1 = await server.fetch('/a')\n  expect(response1.headers.get('Content-Type')).toEqual('text/plain')\n  expect(await response1.text()).toEqual('empty')\n\n  let response2 = await server.fetch('/a', { headers: [['X-Test', '1']] })\n  expect(await response2.text()).toEqual('1')\n\n  let response3 = await server.fetch('/b')\n  expect(response3.status).toEqual(404)\n\n  let response4 = await server.fetch('/a', { method: 'POST' })\n  expect(response4.status).toEqual(404)\n})\n\nit('does not block login because of bruteforce', async () => {\n  server = new TestServer()\n  server.auth(({ userId }) => {\n    return userId === 'good'\n  })\n  await server.expectWrongCredentials('bad1')\n  await server.expectWrongCredentials('bad2')\n  await server.expectWrongCredentials('bad3')\n  await server.expectWrongCredentials('bad4')\n  await server.expectWrongCredentials('bad5')\n  await server.connect('good')\n})\n\nit('destroys on fatal', () => {\n  server = new TestServer()\n  // @ts-expect-error\n  server.emitter.emit('fatal')\n  // @ts-expect-error\n  expect(server.destroying).toBe(true)\n})\n"
  },
  {
    "path": "test-server/index.d.ts",
    "content": "import type { TestLog, TestTime } from '@logux/core'\n\nimport { BaseServer } from '../base-server/index.js'\nimport type {\n  BaseServerOptions,\n  Logger,\n  ServerMeta\n} from '../base-server/index.js'\nimport type { LoggerOptions } from '../server/index.js'\nimport type { TestClient, TestClientOptions } from '../test-client/index.js'\n\nexport interface TestServerOptions extends Omit<\n  BaseServerOptions,\n  'minSubprotocol' | 'subprotocol'\n> {\n  /**\n   * Disable built-in auth.\n   */\n  auth?: false\n\n  /**\n   * Logger with custom settings.\n   */\n  logger?: Logger | LoggerOptions\n\n  minSubprotocol?: number\n\n  subprotocol?: number\n}\n\n/**\n * Server to be used in test.\n *\n * ```js\n * import { TestServer } from '@logux/server'\n * import usersModule from './users.js'\n *\n * let server\n * afterEach(() => {\n *   if (server) server.destroy()\n * })\n *\n * it('connects to the server', () => {\n *   server = new TestServer()\n *   usersModule(server)\n *   let client = await server.connect('10')\n * })\n * ```\n */\nexport class TestServer<\n  Headers extends object = unknown\n> extends BaseServer<Headers> {\n  /**\n   * fetch() compatible API to test HTTP endpoints.\n   *\n   * ```js\n   * server.http('GET', '/version', (req, res) => {\n   *   res.end('1.0.0')\n   * })\n   * let res = await server.fetch()\n   * expect(await res.text()).toEqual('1.0.0')\n   * ```\n   */\n  fetch: typeof fetch\n\n  /**\n   * Server actions log, with methods to check actions inside.\n   *\n   * ```js\n   * server.log.actions() //=> […]\n   * ```\n   */\n  log: TestLog<ServerMeta>\n\n  /**\n   * Time replacement without variable parts like current timestamp.\n   */\n  time: TestTime\n\n  /**\n   * @param opts The limit subset of server options.\n   */\n  constructor(opts?: TestServerOptions)\n\n  /**\n   * Create and connect client.\n   *\n   * ```js\n   * server = new TestServer()\n   * let client = await server.connect('10')\n   * ```\n   *\n   * @param userId User ID.\n   * @param opts Other options.\n   * @returns Promise with new client.\n   */\n  connect(userId: string, opts?: TestClientOptions): Promise<TestClient>\n\n  /**\n   * Call callback and throw an error if there was no `Action was denied`\n   * during callback.\n   *\n   * ```js\n   * await server.expectDenied(async () => {\n   *   client.subscribe('secrets')\n   * })\n   * ```\n   *\n   * @param test Callback with subscripting or action sending.\n   */\n  expectDenied(test: () => unknown): Promise<void>\n\n  /**\n   * Call callback and throw an error if there was no error during\n   * server processing.\n   *\n   * @param text RegExp or string of error message.\n   * @param test Callback with subscripting or action sending.\n   */\n  expectError(text: RegExp | string, test: () => unknown): Promise<void>\n\n  /**\n   * Call callback and throw an error if there was no `logux/undo` in return\n   * with specific reason.\n   *\n   * ```js\n   * await server.expectUndo('notFound', async () => {\n   *   client.subscribe('projects/nothing')\n   * })\n   * ```\n   *\n   * @param reason The reason in undo action.\n   * @param test Callback with subscripting or action sending.\n   */\n  expectUndo(reason: string, test: () => unknown): Promise<void>\n\n  /**\n   * Try to connect client and throw an error is client didn’t received\n   * `Wrong Cregentials` message from the server.\n   *\n   * ```js\n   * server = new TestServer()\n   * await server.expectWrongCredentials('10')\n   * ```\n   *\n   * @param userId User ID.\n   * @param opts Other options.\n   * @returns Promise until check.\n   */\n  expectWrongCredentials(\n    userId: string,\n    opts?: TestClientOptions\n  ): Promise<void>\n}\n"
  },
  {
    "path": "test-server/index.js",
    "content": "import { TestTime } from '@logux/core'\nimport { createServer } from 'node:http'\n\nimport { BaseServer } from '../base-server/index.js'\nimport { createReporter } from '../create-reporter/index.js'\nimport { TestClient } from '../test-client/index.js'\n\nexport class TestServer extends BaseServer {\n  constructor(opts = {}) {\n    if (!opts.time) {\n      opts.time = new TestTime()\n    }\n\n    opts.time.lastId += 1\n    super({\n      id: `${opts.time.lastId}`,\n      minSubprotocol: 0,\n      subprotocol: 0,\n      ...opts\n    })\n    if (opts.logger) {\n      this.on('report', createReporter(opts))\n    } else {\n      this.logger = {\n        debug: () => {},\n        error: () => {},\n        fatal: () => {},\n        info: () => {},\n        warn: () => {}\n      }\n    }\n    if (opts.auth !== false) this.auth(() => true)\n    this.testUsers = {}\n\n    this.fetch = this.fetch.bind(this)\n\n    this.on('fatal', async () => {\n      await this.destroy()\n    })\n  }\n\n  async connect(userId, opts = {}) {\n    let client = new TestClient(this, userId, opts)\n    await client.connect()\n    return client\n  }\n\n  async expectDenied(test) {\n    await this.expectUndo('denied', test)\n  }\n\n  async expectError(text, test) {\n    try {\n      await test()\n      throw new Error('Actions passed without error')\n    } catch (e) {\n      if (\n        (typeof text === 'string' && e.message !== text) ||\n        (text instanceof RegExp && !text.test(e.message))\n      ) {\n        throw e\n      }\n    }\n  }\n\n  async expectUndo(reason, test) {\n    try {\n      await test()\n      throw new Error('Actions passed without error')\n    } catch (e) {\n      if (reason === 'denied' && e.message === 'Action was denied') return\n      if (e.message === 'Server undid action') {\n        if (e.action.reason !== reason) {\n          throw new Error(\n            `Undo was with ${e.action.reason} reason, not ${reason}`,\n            { cause: e }\n          )\n        }\n      } else {\n        throw e\n      }\n    }\n  }\n\n  async expectWrongCredentials(userId, opts = {}) {\n    try {\n      await this.connect(userId, opts)\n      throw new Error('Credentials passed')\n    } catch (e) {\n      if (e.message !== 'Wrong credentials') {\n        throw e\n      }\n    }\n  }\n\n  async fetch(path, init) {\n    let server = createServer(async (req, res) => {\n      await this.processHttp(req, res)\n      server.close()\n    })\n    await new Promise(resolve => {\n      server.listen(0, resolve)\n    })\n    let { port } = server.address()\n    return fetch(`http://localhost:${port}${path}`, init)\n  }\n\n  isBruteforce() {\n    return false\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2024\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"allowJs\": true,\n    \"strict\": true,\n    \"noEmit\": true\n  },\n  \"exclude\": [\"**/errors.ts\"]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      exclude: [\n        'node_modules',\n        'server/index.js',\n        'test/*',\n        '**/*.d.ts',\n        '**/*.test.ts',\n        '*/errors.ts',\n        '*/types.ts',\n        '*.config.*',\n        'human-formatter'\n      ],\n      provider: 'v8',\n      thresholds: {\n        lines: 100\n      }\n    },\n    environment: 'node',\n    exclude: ['node_modules', 'test/servers']\n  }\n})\n"
  }
]