Full Code of logux/logux-server for AI

main ee982148be70 cached
86 files
356.2 KB
110.6k tokens
415 symbols
1 requests
Download .txt
Showing preview only (380K chars total). Download the full file or copy to clipboard to get everything.
Repository: logux/logux-server
Branch: main
Commit: ee982148be70
Files: 86
Total size: 356.2 KB

Directory structure:
gitextract_s1og6kor/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── api.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── add-http-pages/
│   ├── hello.html
│   ├── index.js
│   └── index.test.ts
├── add-sync-map/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── allowed-meta/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── base-server/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── context/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── create-http-server/
│   └── index.js
├── create-reporter/
│   ├── __snapshots__/
│   │   └── index.test.ts.snap
│   ├── index.js
│   └── index.test.ts
├── filter-meta/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── filtered-node/
│   ├── index.js
│   └── index.test.ts
├── human-formatter/
│   ├── index.js
│   └── utils.js
├── index.d.ts
├── index.js
├── options-loader/
│   ├── __snapshots__/
│   │   └── index.test.js.snap
│   ├── index.js
│   ├── index.test.js
│   └── test.env
├── oxfmt.config.ts
├── oxlint.config.ts
├── package.json
├── request/
│   ├── index.d.ts
│   └── index.js
├── server/
│   ├── __snapshots__/
│   │   └── index.test.ts.snap
│   ├── errors.ts
│   ├── index.d.ts
│   ├── index.js
│   ├── index.test.ts
│   └── types.ts
├── server-client/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── test/
│   ├── fixtures/
│   │   ├── cert.pem
│   │   └── key.pem
│   ├── force-colors.js
│   └── servers/
│       ├── autoload-error-modules.js
│       ├── autoload-modules.js
│       ├── destroy.js
│       ├── eacces.js
│       ├── eaddrinuse.js
│       ├── error-modules/
│       │   └── wrond-export/
│       │       └── index.js
│       ├── json.js
│       ├── logger.js
│       ├── missed.js
│       ├── modules/
│       │   ├── child/
│       │   │   ├── index.foo.js
│       │   │   ├── index.js
│       │   │   └── lib/
│       │   │       └── lib.js
│       │   ├── root.js
│       │   └── root.test.js
│       ├── options.js
│       ├── root.js
│       ├── throw.js
│       ├── unbind.js
│       ├── uncatch.js
│       └── unknown.js
├── test-client/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── test-server/
│   ├── index.d.ts
│   └── index.js
├── tsconfig.json
└── vite.config.ts

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

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

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


================================================
FILE: .github/workflows/api.yml
================================================
name: Update API
on:
  create:
    tags:
      - '*.*.*'
permissions: {}
jobs:
  api:
    runs-on: ubuntu-latest
    steps:
      - name: Start logux.org re-build
        run: |
          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"}'


================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
  push:
    tags:
      - '*'
permissions:
  contents: write
jobs:
  release:
    name: Release On Tag
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Extract the changelog
        id: changelog
        run: |
          TAG_NAME=${GITHUB_REF/refs\/tags\//}
          READ_SECTION=false
          CHANGELOG=""
          while IFS= read -r line; do
            if [[ "$line" =~ ^#+\ +(.*) ]]; then
              if [[ "${BASH_REMATCH[1]}" == "$TAG_NAME" ]]; then
                READ_SECTION=true
              elif [[ "$READ_SECTION" == true ]]; then
                break
              fi
            elif [[ "$READ_SECTION" == true ]]; then
              CHANGELOG+="$line"$'\n'
            fi
          done < "CHANGELOG.md"
          CHANGELOG=$(echo "$CHANGELOG" | awk '/./ {$1=$1;print}')
          echo "changelog_content<<EOF" >> $GITHUB_OUTPUT
          echo "$CHANGELOG" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
      - name: Create the release
        if: steps.changelog.outputs.changelog_content != ''
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
        with:
          name: ${{ github.ref_name }}
          body: '${{ steps.changelog.outputs.changelog_content }}'
          draft: false
          prerelease: false


================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
  push:
    branches:
      - main
      - next
  pull_request:
permissions:
  contents: read
jobs:
  full:
    name: Node.js Latest Full
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Install pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
        with:
          version: 10
      - name: Install Node.js
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version: 25
          cache: pnpm
      - name: Install dependencies
        run: pnpm install --ignore-scripts
      - name: Run tests
        run: pnpm test
  short:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version:
          - 24
          - 22
    name: Node.js ${{ matrix.node-version }} Quick
    steps:
      - name: Checkout the repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Install pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
        with:
          version: 10
      - name: Install Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version: ${{ matrix.node-version }}
          cache: pnpm
      - name: Install dependencies
        run: pnpm install --ignore-scripts
      - name: Run unit tests
        run: pnpm vitest run


================================================
FILE: .gitignore
================================================
node_modules/

coverage/


================================================
FILE: .npmignore
================================================
test/
coverage/
tsconfig.json
**/*.test.ts
**/types.ts
**/errors.ts

__snapshots__


================================================
FILE: .prettierrc.js
================================================
import loguxOxfmtConfig from '@logux/oxc-configs/fmt'

export default loguxOxfmtConfig


================================================
FILE: CHANGELOG.md
================================================
# Change Log

This project adheres to [Semantic Versioning](http://semver.org/).

## 0.14 “Sliver of Straw”

- Removed Node.js 18 support.
- Removed backend control.
- Moved to number as subprotocol and remove `Context#isSubprotocol()`.
- Moved to Logux Core 0.10 and Logux Protocol 5.
- Changed custom HTTP listener API.
- Added API to use custom Logux server node class.
- Added `Server#sendOnConnect()`.
- Reduced dependencies.
- Changed color auto-detection algorithm to `util.styleText`.
- Fixed brute-force lock issue during tests.

## 0.13.1

- Fixed vulnerability audit by moving to `cookie` 0.7.

## 0.13 “Seven Red Suns”

- Removed Node.js 14 and Node.js 16 support.
- Moved to Logux Core 0.9.
- Added action processing queues (by @VladBrok).
- Added `unauthenticated` event (by @erictheswift).

## 0.12.10

- Fixed another Node.js 14 regression.

## 0.12.9

- Fixed Node.js 14 regression.

## 0.12.8

- Replaced `ip` to fix vulnerability.
- Updated dependencies.

## 0.12.7

- Moved `ip` to `2.x` to fix vulnerability.

## 0.12.6

- Fixed `x/changed` filter in `addSyncMap` (by Eduard Aksamitov).

## 0.12.5

- Fixed async action’s filter in channel (by Eduard Aksamitov).

## 0.12.4

- Fixed docs.

## 0.12.3

- Fixed multiple subscriptions with filters per node (by Eduard Aksamitov).
- Fixed types (by Nikita Galaiko).

## 0.12.2

- Fixed `since` in `load` of `addSyncMap` (by Nikita Galaiko).

## 0.12.1

- Fixed `since` in `initial` of `addSyncMapFilter` (by Nikita Galaiko).

## 0.12 “Looks to the Moon”

- Dropped Node.js 12 support.
- Moved to Logux Core 0.8.
- Moved to `pino` 8.
- Added `disableHttpServer` option.
- Added `return false` support to `load` callback in `addSyncMap`.
- Fixed data loading on subscription on `SyncMap` creation.

## 0.11 “Five Pebbles”

- Added `addSyncMap()` and `addSyncMapFilter()`.
- Added colorization to action ID and client ID (by Bijela Gora).
- Added `TestServer#expectError()`.
- Added `since` to `TestClient#subscribe()`.
- Reduced noise in server log.
- Moved to `pino` 7 (by Bijela Gora).

## 0.10.8

- Fixed test server destroying on fatal error.

## 0.10.7

- Reduced dependencies.

## 0.10.6

- Fixed `Promise` support in channel’s `filter` (by Eduard Aksamitov).
- Replaced `nanocolors` with `picocolors`.

## 0.10.5

- Fixed `Server#http()`.
- Fixed types (by Eduard Aksamitov).

## 0.10.4

- Updated `nanocolors`.

## 0.10.3

- Replaced `colorette` with `nanocolors`.

## 0.10.2

- Fixed `accessAndProcess` on server’s action (by Aleksandr Slepchenkov).
- Added warning about circular reference in action.
- Marked `action` and `meta` in callbacks as read-only.

## 0.10.1

- Fixed channel name parameters parsing (by Aleksandr Slepchenkov).
- Used `LoguxNotFoundError` from `@logux/actions`.

## 0.10 “Doraemon”

- Moved project to ESM-only type. Applications must use ESM too.
- Dropped Node.js 10 support.
- Moved health check to `/health`.
- Added `Server#http()` for custom HTTP processing.
- Added `unsubscribe` callback to `Server#channel` (by @erictheswift).
- Added reverted action to `logux/undo` (by Eduard Aksamitov).
- Added RegExp support to `BaseServer#type()` (by Taras Vozniuk).
- Added `accessAndLoad` and `accessAndProcess` callbacks for REST integration.
- Added `LoguxNotFoundError` error for `accessAndLoad` and `accessAndProcess`.
- Added request functions and `wasNot403()` for REST integration.
- Added `ServerClient#httpHeaders`.
- Added support for returning string from `resend` callback.
- Added `Server#subscribe()` to send `logux/subscribed` action.
- Added `Server#autoloadModules()`.
- Added `fileUrl` option for ESM servers.
- Added `Server#logger` for custom log messages.
- Added `meta.excludeClients`.
- Added `TestServer#expectUndo()`.
- Added `TestServer#expectDenied()`.
- Added `TestClient#received()`.
- Added `TestServer#expectWrongCredentials()`.
- Added `TestClient#clientId` and `TestClient#userId`.
- Added `filter` option to `TestClient#subscribe()`.
- Added Logux logotype to `GET /`.
- Removed `reporter` option (by Aleksandr Slepchenkov).
- Removed `yargs` dependency (by Aleksandr Slepchenkov).
- Fixed `:` symbol support for channel names.
- Fixed types performance by replacing `type` to `interface`.

## 0.9.6

- Update `yargs`.

## 0.9.5

- Fixed sending server’s actions to backend.

## 0.9.4

- Fix using old action’s IDs in `Server#channel→load`.

## 0.9.3

- Do not process actions from `Server#channel→load` in `Server#type`.
- Replace color output library.

## 0.9.2

- Fix cookie support (by Eduard Aksamitov).

## 0.9.1

- Reduce dependencies.

## 0.9 “Robby the Robot”

- Use WebSocket Protocol version 4.
- Use Back-end Protocol version 4.
- Replace `bunyan` logger with `pino` (by Alexander Slepchenkov).
- Clean up logger options (by Alexander Slepchenkov).
- Allow to return actions from `load` callback.
- Add cookie-based authentication.
- Add `Server#process()`.
- Allow to use action creator in `Server#type()`.
- Add `LOGUX_SUBPROTOCOL` and `LOGUX_SUPPORTS` environment variables support.
- Add `Server#autoloadModules()` (by Andrey Berezhnoy).
- Add `Context#headers`.
- Add argument to `TestServer#connect()`.
- Add `auth: false` option to `TestServer`.
- Fix action double sending.
- Fix infinite reconnecting on authentication error.
- Fix multiple servers usage in tests.
- Fix types.

## 0.8.6

- Add `BaseServer#options` types.

## 0.8.5

- `Context#sendBack` returns Promise until action will be re-send and processed.
- Fix `Context#sendBack` typings.

## 0.8.4

- Fix back-end protocol check in HTTP request receiving.

## 0.8.3

- Make node IDs in `TestClient` shorter.

## 0.8.2

- Fix types.

## 0.8.1

- Call `resend` after `access` step in action processing.
- Add special reason for unknown action or channel errors.
- Fix `TestClient` error on unknown action or channel.
- Allow to show log by passing `reporter: "human"` option to `TestServer`.
- Fix calling `resend` on server’s own actions.
- Fix types (by Andrey Berezhnoy).

## 0.8 “Morpheus”

- Rename `init` callback to `load` in `Server#channel()`.
- Add `TestServer` and `TestClient` to test servers.
- Add `filterMeta` helper.
- Fix types.

## 0.7.2

- More flexible types for logger.

## 0.7.1

- Print to the log about denied control requests attempts.
- Fix server options types.
- Return status code 500 on control requests if server has no secret.

## 0.7 “Eliza Cassan”

- Use Logux Core 0.5 and WebSocket Protocol 3.
- Use Back-end Protocol 3.
- Use the same port for WebSocket and control.
- Rename `LOGUX_CONTROL_PASSWORD` to `LOGUX_CONTROL_SECRET`.
- Rename `opts.controlPassword` to `opts.controlSecret`.
- User ID must be always a string.
- Add IP address check for control requests.
- Fix types.

## 0.6.1

- Keep context between steps.
- Fix re-sending actions back to the author.

## 0.6 “Helios”

- Add ES modules support.
- Add TypeScript definitions (by Kirill Neruchev).
- Move API docs from JSDoc to TypeDoc.

## 0.5.3

- Fix Nano Events API.

## 0.5.2

- Fix subscriptions for clients follower.

## 0.5.1

- Fix JSDoc.

## 0.5 “Icarus”

- Add `Context#sendBack()` shortcut.
- Add `finally` callback to `Server#type()`. and `Server#channel()`.
- Add `resend` callback to `Server#type()`.
- Use Backend Protocol 2.
- Deny any re-send meta keys from clients (like `channels`).
- Add singular re-send meta keys support (`channel`, `client`, etc).
- Allow to listen `preadd` and `add` log events in `Server#on()`.
- Use `error` as default reason in `Server#undo()`.
- Set boolean `false` user ID on client IDs like `false:client:uuid`.

## 0.4 “Daedalus”

- Add `.env` support.

## 0.3.4

- Update dependencies.

## 0.3.3

- Improve popular error messages during server launch (by Igor Strebezhev).

## 0.3.2

- Fix backend proxy version (by Dmitry Salahutdinov).
- Clean up code (by Vladimir Schedrin).

## 0.3.1

- Fix support for `unknownAction` and `unknownChannel` commands from backend.

## 0.3 “SHODAN”

- Rename project from `logux-server` to `@logux/server`.
- Rename `meta.nodeIds` to `meta.nodes`.
- Rename `Server#clients` to `Server#connected`.
- Rename `Server#users` to `Server#userIds`.
- Split subscription to `access`, `init`, and `filter` steps.
- Add `ctx` to callbacks.
- Remove Node.js 6 and 8 support.
- `Server.loadOptions` now overrides default options.
- Change default port from `:1337` to `:31337`.
- Use Logux Core 0.3.
- Add brute force protection.
- Add built-in proxy mode.
- Add HTTP health check API.
- Answer `logux/processed` after action processing.
- Add `ServerClient#clientId` and `meta.clients`.
- Add warning about missed action callbacks.

## 0.2.9

- Use `ws` instead of `uWS`.

## 0.2.8

- Add protection against authentication brute force.

## 0.2.7

- Use `uWS` 9.x with Node.js 10 support.

## 0.2.6

- Use `yargs` 11.x.

## 0.2.5

- Allow to have `:` in user ID.

## 0.2.4

- Use `uWS` 9.x.

## 0.2.3

- Fix `key` option with `{ pem: … }` value on Node.js 9.

## 0.2.2

- Don’t destroy server again on error during destroy.

## 0.2.1

- Don’t show `unknownType` error on server actions without processor.
- Better action and meta view in `human` log.

## 0.2 “Neuromancer”

- Use Logux Protocol 2.
- Use Logux Core 0.2 and Logux Sync 0.2.
- Rename `Client#id` to `Client#userId`.
- Remove `BaseServer#once` method.
- Check action’s node ID to have user ID.
- Use `uws` instead of `ws` (by Anton Savoskin).
- Use Nano ID for node ID.
- Remove deprecated `upgradeReq` from `Client#remoteAddess`.
- Use Chalk 2.0.
- Add `BaseServer#type` method.
- Add `BaseServer#channel` method.
- Add `BaseServer#undo` method.
- Add `BaseServer#sendAction` method.
- Take options from CLI and environment variables (by Pavel Kovalyov).
- Add production non-secure protocol warning (by Hanna Stoliar).
- Add Bunyan log format support (by Anton Artamonov and Mateusz Derks).
- Add `error` event.
- Set `meta.server`, `meta.status` and `meta.subprotocol`.
- Add `debug` message support (by Roman Fursov).
- Add `BaseServer#nodeId` shortcut.
- Add node ID conflict fixing.
- Export `ALLOWED_META`.
- Better start error description (by Grigory Moroz).
- Show Client ID in log for non-authenticated users.
- Fix docs (by Grigoriy Beziuk, Nick Mitin and Konstantin Krivlenia).
- Always use English for `--help` message.
- Add security note for server output in development mode.

## 0.1.1

- Fix custom HTTP server support.

## 0.1 “Wintermute”

- Initial release.


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

Copyright 2016 Andrey Sitnik <andrey@sitnik.es>

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

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

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


================================================
FILE: README.md
================================================
# Logux Server [![Cult Of Martians][cult-img]][cult]

<img align="right" width="95" height="148" title="Logux logotype"
     src="https://logux.org/branding/logotype.svg">

Logux is a new way to connect client and server. Instead of sending
HTTP requests (e.g., AJAX and GraphQL) it synchronizes log of operations
between client, server, and other clients.

- **[Guide, recipes, and API](https://logux.org/)**
- **[Issues](https://github.com/logux/logux/issues)**
  and **[roadmap](https://github.com/orgs/logux/projects/1)**
- **[Projects](https://logux.org/guide/architecture/parts/)**
  inside Logux ecosystem

This repository contains Logux server with:

- Framework to write own server.
- Proxy between WebSocket and HTTP server on any other language.

<a href="https://evilmartians.com/?utm_source=logux-server">
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg"
       alt="Sponsored by Evil Martians" width="236" height="54">
</a>

[cult-img]: http://cultofmartians.com/assets/badges/badge.svg
[cult]: http://cultofmartians.com/done.html

### Logux Server as Framework

```js
import { isFirstOlder } from '@logux/core'
import { dirname } from 'path'
import { Server } from '@logux/server'

const server = new Server(
  Server.loadOptions(process, {
    subprotocol: 1,
    minSubprotocol: 1,
    root: import.meta.dirname
  })
)

server.auth(async ({ userId, token }) => {
  const user = await findUserByToken(token)
  return !!user && userId === user.id
})

server.channel('user/:id', {
  access(ctx, action, meta) {
    return ctx.params.id === ctx.userId
  },
  async load(ctx, action, meta) {
    const user = await db.loadUser(ctx.params.id)
    return { type: 'USER_NAME', name: user.name }
  }
})

server.type('CHANGE_NAME', {
  access(ctx, action, meta) {
    return action.user === ctx.userId
  },
  resend(ctx, action, meta) {
    return { channel: `user/${ctx.userId}` }
  },
  async process(ctx, action, meta) {
    if (isFirstOlder(lastNameChange(action.user), meta)) {
      await db.changeUserName({ id: action.user, name: action.name })
    }
  }
})

server.listen()
```

[documentation]: https://logux.org/


================================================
FILE: add-http-pages/hello.html
================================================
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Logux Server</title>
    <style>
      html {
        height: 100%;
      }
      body {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 100%;
        margin: 0;
      }
      svg {
        height: 25vh;
      }
    </style>
  </head>
  <body>
    <a href="https://logux.org/">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75 117">
        <g fill="none" fill-rule="evenodd">
          <path d="M-1 0h78v117H-1z" />
          <g fill-rule="nonzero">
            <path
              fill="#1F1F1F"
              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"
            />
            <path
              fill="#F5A623"
              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"
            />
          </g>
          <path
            fill="#1F1F1F"
            fill-rule="nonzero"
            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"
          />
        </g>
      </svg>
    </a>
  </body>
</html>


================================================
FILE: add-http-pages/index.js
================================================
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'

let hello
async function readHello() {
  if (!hello) {
    hello = await readFile(join(import.meta.dirname, 'hello.html'))
  }
  return hello
}

export function addHttpPages(server) {
  if (!server.options.disableHttpServer) {
    server.http('GET', '/', async (req, res) => {
      let data = await readHello()
      res.writeHead(200, { 'Content-Type': 'text/html' })
      res.end(data)
    })
    server.http('GET', '/health', (req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' })
      res.end('Logux Server: OK\n')
    })
  }
}


================================================
FILE: add-http-pages/index.test.ts
================================================
import { type TestLog, TestTime } from '@logux/core'
import http from 'node:http'
import { setTimeout } from 'node:timers/promises'
import { afterEach, expect, it } from 'vitest'

import {
  BaseServer,
  type BaseServerOptions,
  type ServerMeta
} from '../index.js'

const DEFAULT_OPTIONS = {
  minSubprotocol: 0,
  subprotocol: 0
}

let lastPort = 9111

function createServer(
  options: Partial<BaseServerOptions> = {}
): BaseServer<object, TestLog<ServerMeta>> {
  let opts = {
    ...DEFAULT_OPTIONS,
    ...options
  }
  if (typeof opts.time === 'undefined') {
    opts.time = new TestTime()
    opts.id = 'uuid'
  }
  if (typeof opts.port === 'undefined') {
    lastPort += 1
    opts.port = lastPort
  }

  let created = new BaseServer<object, TestLog<ServerMeta>>(opts)
  created.auth(() => true)

  destroyable = created

  return created
}

let destroyable: BaseServer | undefined

class RequestError extends Error {
  statusCode: number | undefined

  constructor(statusCode: number | undefined, body: string) {
    super(body)
    this.name = 'RequestError'
    this.statusCode = statusCode
  }
}

interface HttpResponse {
  body: string
  headers: http.IncomingHttpHeaders
}

function request(
  server: BaseServer,
  method: string,
  path: string
): Promise<HttpResponse> {
  return new Promise<HttpResponse>((resolve, reject) => {
    let req = http.request(
      {
        host: '127.0.0.1',
        method,
        path,
        port: server.options.port
      },
      res => {
        let body = ''
        res.on('data', chunk => {
          body += chunk
        })
        res.on('end', () => {
          if (res.statusCode === 200) {
            resolve({ body, headers: res.headers })
          } else {
            let error = new RequestError(res.statusCode, body)
            reject(error)
          }
        })
      }
    )
    req.on('error', reject)
    req.end()
  })
}

async function requestError(
  server: BaseServer,
  method: string,
  path: string
): Promise<RequestError> {
  try {
    await request(server, method, path)
  } catch (e) {
    if (e instanceof RequestError) return e
  }
  throw new Error('Error was not found')
}

afterEach(async () => {
  if (destroyable) {
    await destroyable.destroy()
    destroyable = undefined
  }
})

it('has hello page', async () => {
  let app = createServer({})
  await app.listen()
  let response = await request(app, 'GET', '/')
  expect(response.body).toContain('Logux Server')
  expect(response.body).toContain('<svg ')
})

it('disables HTTP on request', async () => {
  let app = createServer({ disableHttpServer: true })
  await app.listen()
  let response = false
  let req = http.request(
    {
      host: '127.0.0.1',
      method: 'GET',
      path: '/health',
      port: app.options.port
    },
    () => {
      response = true
    }
  )
  req.on('error', () => {})
  await setTimeout(100)
  expect(response).toBe(false)
  req.destroy()
})

it('has health check', async () => {
  let app = createServer()
  await app.listen()
  let response = await request(app, 'GET', '/health')
  expect(response.body).toContain('OK')
})

it('responses 404', async () => {
  let app = createServer()
  await app.listen()
  let err = await requestError(app, 'GET', '/unknown')
  expect(err.statusCode).toEqual(404)
  expect(err.message).toEqual('Not found\n')
})

it('has custom HTTP processor', async () => {
  let app = createServer()
  let unknownGet = 0
  let unknownRest = 0
  app.http('POST', '/a', (req, res) => {
    res.end('POST a')
  })
  app.http('GET', '/a', (req, res) => {
    res.end('GET a')
  })
  app.http('GET', '/b', (req, res) => {
    res.end('GET b')
  })
  app.http((req, res) => {
    if (req.method === 'GET') {
      res.end('GET unknown')
      unknownGet += 1
      return true
    } else {
      return false
    }
  })
  app.http((req, res) => {
    if (req.url !== '/404') {
      res.end('unknown')
      unknownRest += 1
      return true
    } else {
      return false
    }
  })
  await app.listen()
  expect((await request(app, 'GET', '/a')).body).toContain('GET a')
  expect((await request(app, 'GET', '/a%3Fsecret')).body).toContain('GET a')
  expect((await request(app, 'GET', '/b')).body).toContain('GET b')
  expect((await request(app, 'POST', '/a')).body).toContain('POST a')
  expect((await request(app, 'GET', '/c')).body).toContain('GET unknown')
  expect((await request(app, 'GET', '/d')).body).toContain('GET unknown')
  expect((await request(app, 'POST', '/e')).body).toContain('unknown')
  expect((await requestError(app, 'POST', '/404')).statusCode).toEqual(404)
  expect(unknownGet).toEqual(2)
  expect(unknownRest).toEqual(1)
})

it('warns that HTTP is disables', () => {
  let app = createServer({ disableHttpServer: true })
  expect(() => {
    app.http(() => true)
  }).toThrow(/when `disableHttpServer` enabled/)
})

it('waits until all HTTP processing ends', async () => {
  let app = createServer()
  let resolveA: (() => void) | undefined
  app.http('GET', '/a', () => {
    return new Promise(resolve => {
      resolveA = resolve
    })
  })
  let resolveResult: ((processed: boolean) => void) | undefined
  app.http(() => {
    return new Promise(resolve => {
      resolveResult = resolve
    })
  })
  await app.listen()

  request(app, 'GET', '/a')
  request(app, 'GET', '/other')
  await setTimeout(10)

  let destroyed = false
  app.destroy().then(() => {
    destroyed = true
  })

  await setTimeout(100)
  expect(destroyed).toBe(false)

  expect((await requestError(app, 'POST', '/a')).message).toEqual(
    'The server is shutting down\n'
  )

  resolveA!()
  await setTimeout(100)
  expect(destroyed).toBe(false)

  resolveResult!(true)
  await setTimeout(100)
  expect(destroyed).toBe(true)
})


================================================
FILE: add-sync-map/index.d.ts
================================================
import type {
  LoguxSubscribeAction,
  SyncMapChangeAction,
  SyncMapChangedAction,
  SyncMapCreateAction,
  SyncMapCreatedAction,
  SyncMapDeleteAction,
  SyncMapDeletedAction,
  SyncMapTypes,
  SyncMapValues
} from '@logux/actions'

import type { BaseServer, ServerMeta } from '../base-server/index.js'
import type { Context } from '../context/index.js'

declare const WITH_TIME: unique symbol

export type WithTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
  time: number
  value: Value
  [WITH_TIME]: true
}

export type WithoutTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
  time: undefined
  value: Value
  [WITH_TIME]: false
}

export type SyncMapData<Value extends SyncMapValues> = {
  [Key in keyof Value]: WithoutTime<Value[Key]> | WithTime<Value[Key]>
} & { id: string }

/**
 * Add last changed time to value to use in conflict resolution.
 *
 * If you do not know the time, use {@link NoConflictResolution}.
 *
 * @param value The value.
 * @param time UNIX milliseconds.
 * @returns Wrapper.
 */
export function ChangedAt<Value extends SyncMapTypes | SyncMapTypes[]>(
  value: Value,
  time: number
): WithTime<Value>

/**
 * Mark that the value has no last changed date and conflict resolution
 * can’t be applied.
 *
 * @param value The value.
 * @returns Wrapper.
 */
export function NoConflictResolution<
  Value extends SyncMapTypes | SyncMapTypes[]
>(value: Value): WithTime<Value>

interface SyncMapActionFilter<Value extends SyncMapValues> {
  (
    ctx: Context,
    action:
      | SyncMapChangedAction<Value>
      | SyncMapCreatedAction<Value>
      | SyncMapDeletedAction,
    meta: ServerMeta
  ): boolean | Promise<boolean>
}

interface SyncMapOperations<Value extends SyncMapValues> {
  access(
    ctx: Context,
    id: string,
    action:
      | LoguxSubscribeAction
      | SyncMapChangeAction
      | SyncMapCreateAction
      | SyncMapDeleteAction,
    meta: ServerMeta
  ): boolean | Promise<boolean>

  change?(
    ctx: Context,
    id: string,
    fields: Partial<Value>,
    time: number,
    action: SyncMapChangeAction<Value>,
    meta: ServerMeta
  ): boolean | Promise<boolean | void> | void

  create?(
    ctx: Context,
    id: string,
    fields: Value,
    time: number,
    action: SyncMapCreateAction<Value>,
    meta: ServerMeta
  ): boolean | Promise<boolean | void> | void

  delete?(
    ctx: Context,
    id: string,
    action: SyncMapDeleteAction,
    meta: ServerMeta
  ): boolean | Promise<boolean | void> | void

  load?(
    ctx: Context,
    id: string,
    since: number | undefined,
    action: LoguxSubscribeAction,
    meta: ServerMeta
  ): false | Promise<false | SyncMapData<Value>> | SyncMapData<Value>
}

interface SyncMapFilterOperations<Value extends SyncMapValues> {
  access?(
    ctx: Context,
    filter: Partial<Value> | undefined,
    action: LoguxSubscribeAction,
    meta: ServerMeta
  ): boolean | Promise<boolean>

  actions?(
    ctx: Context,
    filter: Partial<Value> | undefined,
    action: LoguxSubscribeAction,
    meta: ServerMeta
  ): Promise<SyncMapActionFilter<Value>> | SyncMapActionFilter<Value> | void

  initial(
    ctx: Context,
    filter: Partial<Value> | undefined,
    since: number | undefined,
    action: LoguxSubscribeAction,
    meta: ServerMeta
  ): Promise<SyncMapData<Value>[]> | SyncMapData<Value>[]
}

/**
 * Add callbacks for client’s `SyncMap`.
 *
 * ```js
 * import { addSyncMap, isFirstTimeOlder, ChangedAt } from '@logux/server'
 * import { LoguxNotFoundError } from '@logux/actions'
 *
 * addSyncMap(server, 'tasks', {
 *   async access (ctx, id) {
 *     const task = await Task.find(id)
 *     return ctx.userId === task.authorId
 *   },
 *
 *   async load (ctx, id, since) {
 *     const task = await Task.find(id)
 *     if (!task) throw new LoguxNotFoundError()
 *     return {
 *       id: task.id,
 *       text: ChangedAt(task.text, task.textChanged),
 *       finished: ChangedAt(task.finished, task.finishedChanged),
 *     }
 *   },
 *
 *   async create (ctx, id, fields, time) {
 *     await Task.create({
 *       id,
 *       text: fields.text,
 *       finished: fields.finished,
 *       authorId: ctx.userId,
 *       textChanged: time,
 *       finishedChanged: time
 *     })
 *   },
 *
 *   async change (ctx, id, fields, time) {
 *     const task = await Task.find(id)
 *     if ('text' in fields) {
 *       if (task.textChanged < time) {
 *         await task.update({
 *           text: fields.text,
 *           textChanged: time
 *         })
 *       }
 *     }
 *     if ('finished' in fields) {
 *       if (task.finishedChanged < time) {
 *         await task.update({
 *           finished: fields.finished,
 *           finishedChanged: time
 *         })
 *       }
 *     }
 *   }
 *
 *   async delete (ctx, id) {
 *     await Task.delete(id)
 *   }
 * })
 * ```
 *
 * @param server Server instance.
 * @param plural Prefix for channel names and action types.
 * @param operations Callbacks.
 */
export function addSyncMap<Values extends SyncMapValues>(
  server: BaseServer,
  plural: string,
  operations: SyncMapOperations<Values>
): void

/**
 * Add callbacks for client’s `useFilter`.
 *
 * ```js
 * import { addSyncMapFilter, ChangedAt } from '@logux/server'
 *
 * addSyncMapFilter(server, 'tasks', {
 *   access (ctx, filter) {
 *     return true
 *   },
 *
 *   initial (ctx, filter, since) {
 *     let tasks = await Tasks.where({ ...filter, authorId: ctx.userId })
 *     // You can return only data changed after `since`
 *     return tasks.map(task => ({
 *       id: task.id,
 *       text: ChangedAt(task.text, task.textChanged),
 *       finished: ChangedAt(task.finished, task.finishedChanged),
 *     }))
 *   },
 *
 *   actions (filterCtx, filter) {
 *     return (actionCtx, action, meta) => {
 *       return actionCtx.userId === filterCtx.userId
 *     }
 *   }
 * })
 * ```

 * @param server Server instance.
 * @param plural Prefix for channel names and action types.
 * @param operations Callbacks.
 */
export function addSyncMapFilter<Values extends SyncMapValues>(
  server: BaseServer,
  plural: string,
  operations: SyncMapFilterOperations<Values>
): void


================================================
FILE: add-sync-map/index.js
================================================
const WITH_TIME = Symbol('WITH_TIME')

export function ChangedAt(value, time) {
  return { time, value, [WITH_TIME]: true }
}

export function NoConflictResolution(value) {
  return { value, [WITH_TIME]: false }
}

async function addFinished(server, ctx, type, action, meta) {
  await server.process(
    { ...action, type },
    { excludeClients: [ctx.clientId], time: meta.time }
  )
}

function resendFinished(server, plural, type, all = true) {
  if (all) {
    server.type(type, {
      access() {
        return false
      },
      resend(ctx, action) {
        return [plural, `${plural}/${action.id}`]
      }
    })
  } else {
    server.type(type, {
      access() {
        return false
      },
      resend(ctx, action) {
        return [`${plural}/${action.id}`]
      }
    })
  }
}

function buildFilter(filter) {
  return (ctx, action) => {
    if (action.type.endsWith('/created')) {
      for (let key in filter) {
        if (action.fields[key] !== filter[key]) return false
      }
    }
    if (action.type.endsWith('/changed')) {
      for (let key in filter) {
        if (key in action.fields && action.fields[key] !== filter[key]) {
          return false
        }
      }
    }
    return true
  }
}

async function sendMap(server, changedType, data, since) {
  let { id, ...other } = data
  let byTime = new Map()
  for (let key in other) {
    if (other[key][WITH_TIME] === true) {
      let time = other[key].time
      if (!byTime.has(time)) byTime.set(time, {})
      byTime.get(time)[key] = other[key].value
    } else if (other[key][WITH_TIME] === false) {
      if (!byTime.has('now')) byTime.set('now', {})
      byTime.get('now')[key] = other[key].value
    } else {
      throw new Error('Wrap value into ChangedAt() or NoConflictResolution()')
    }
  }
  for (let [time, fields] of byTime.entries()) {
    let changedMeta
    if (time !== 'now') {
      changedMeta = { time }
      if (time < since) continue
    }
    await server.process(
      {
        fields,
        id,
        type: changedType
      },
      changedMeta
    )
  }
}

export function addSyncMap(server, plural, operations) {
  let createdType = `${plural}/created`
  let changedType = `${plural}/changed`
  let deletedType = `${plural}/deleted`
  resendFinished(server, plural, createdType)
  resendFinished(server, plural, changedType)
  resendFinished(server, plural, deletedType, false)

  if (operations.load) {
    server.channel(`${plural}/:id`, {
      access(ctx, action, meta) {
        return operations.access(ctx, ctx.params.id, action, meta)
      },
      async load(ctx, action, meta) {
        if (action.creating) return
        let since = action.since ? action.since.time : 0
        let data = await operations.load(
          ctx,
          ctx.params.id,
          since,
          action,
          meta
        )
        if (data !== false) {
          await sendMap(server, changedType, data, since)
        }
      }
    })
  }

  if (operations.create) {
    server.type(`${plural}/create`, {
      access(ctx, action, meta) {
        return operations.access(ctx, action.id, action, meta)
      },
      async process(ctx, action, meta) {
        let result = await operations.create(
          ctx,
          action.id,
          action.fields,
          meta.time,
          action,
          meta
        )
        if (result !== false) {
          await addFinished(server, ctx, createdType, action, meta)
        }
      }
    })
  }

  if (operations.change) {
    server.type(`${plural}/change`, {
      access(ctx, action, meta) {
        return operations.access(ctx, action.id, action, meta)
      },
      async process(ctx, action, meta) {
        let result = await operations.change(
          ctx,
          action.id,
          action.fields,
          meta.time,
          action,
          meta
        )
        if (result !== false) {
          await addFinished(server, ctx, changedType, action, meta)
        }
      }
    })
  }

  if (operations.delete) {
    server.type(`${plural}/delete`, {
      access(ctx, action, meta) {
        return operations.access(ctx, action.id, action, meta)
      },
      async process(ctx, action, meta) {
        let result = await operations.delete(ctx, action.id, action, meta)
        if (result !== false) {
          await addFinished(server, ctx, deletedType, action, meta)
        }
      }
    })
  }
}

export function addSyncMapFilter(server, plural, operations) {
  let changedType = `${plural}/changed`

  server.channel(plural, {
    access(ctx, action, meta) {
      return operations.access(ctx, action.filter, action, meta)
    },
    filter(ctx, action, meta) {
      let filter = action.filter ? buildFilter(action.filter) : () => true
      let custom = operations.actions
        ? operations.actions(ctx, action.filter, action, meta)
        : () => true
      return (ctx2, action2, meta2) => {
        return filter(ctx2, action2, meta2) && custom(ctx2, action2, meta2)
      }
    },
    async load(ctx, action, meta) {
      let since = action.since ? action.since.time : 0
      let data = await operations.initial(
        ctx,
        action.filter,
        since,
        action,
        meta
      )
      await Promise.all(
        data.map(async i => {
          await server.subscribe(ctx.nodeId, `${plural}/${i.id}`)
          await sendMap(server, changedType, i, since)
        })
      )
    }
  })
}


================================================
FILE: add-sync-map/index.test.ts
================================================
import {
  defineSyncMapActions,
  LoguxNotFoundError,
  loguxProcessed,
  loguxSubscribed
} from '@logux/actions'
import { setTimeout } from 'node:timers/promises'
import { afterEach, expect, it } from 'vitest'

import {
  addSyncMap,
  addSyncMapFilter,
  ChangedAt,
  NoConflictResolution,
  type SyncMapData,
  type TestClient,
  TestServer
} from '../index.js'

type TaskValue = {
  finished: boolean
  text: string
}

type TaskRecord = {
  finishedChanged: number
  textChanged: number
} & TaskValue

let [
  createTask,
  changeTask,
  deleteTask,
  createdTask,
  changedTask,
  deletedTask
] = defineSyncMapActions('tasks')

type CommentValue = {
  author?: string
  text?: string
}
let [
  createComment,
  changeComment,
  deleteComment,
  createdComment,
  changedComment,
  deletedComment
] = defineSyncMapActions('comments')

let tasks = new Map<string, TaskRecord>()

let destroyable: TestServer | undefined

function getTime(client: TestClient, creator: { type: string }): number[] {
  return client.log
    .entries()
    .filter(([action]) => action.type === creator.type)
    .map(([, meta]) => meta.time)
}

function getServer(): TestServer {
  let server = new TestServer()
  destroyable = server
  addSyncMap<TaskValue>(server, 'tasks', {
    access(ctx, id, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      return ctx.userId !== 'wrong' && id !== 'bad'
    },
    change(ctx, id, fields, time, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      expect(typeof ctx.userId).toBe('string')
      let task = tasks.get(id)!
      if (
        typeof fields.finished !== 'undefined' &&
        task.finishedChanged < time
      ) {
        task.finished = fields.finished
        task.finishedChanged = time
      }
      if (typeof fields.text !== 'undefined' && task.textChanged < time) {
        task.text = fields.text
        task.textChanged = time
      }
    },
    create(ctx, id, fields, time, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      expect(typeof ctx.userId).toBe('string')
      tasks.set(id, {
        ...fields,
        finishedChanged: time,
        textChanged: time
      })
    },
    delete(ctx, id, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      expect(typeof ctx.userId).toBe('string')
      tasks.delete(id)
    },
    load(ctx, id, since, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      expect(typeof ctx.userId).toBe('string')
      let task = tasks.get(id)
      if (!task) throw new LoguxNotFoundError()
      return {
        finished: ChangedAt(task.finished, task.finishedChanged),
        id,
        text: ChangedAt(task.text, task.textChanged)
      }
    }
  })
  addSyncMapFilter<TaskValue>(server, 'tasks', {
    access(ctx, filter, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      if (ctx.userId === 'wrong') return false
      if (filter?.text) return false
      return true
    },
    actions(ctx, filter, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      return (ctx2, action2) => action2.id !== 'silence'
    },
    initial(ctx, filter, since, action, meta) {
      expect(typeof action.type).toBe('string')
      expect(typeof meta.id).toBe('string')
      let selected: SyncMapData<TaskValue>[] = []
      for (let [id, task] of tasks.entries()) {
        if (filter) {
          let filterKeys = Object.keys(filter) as (keyof TaskValue)[]
          if (filterKeys.some(i => task[i] !== filter[i])) {
            continue
          }
        }
        selected.push({
          finished: ChangedAt(task.finished, task.finishedChanged),
          id,
          text: ChangedAt(task.text, task.textChanged)
        })
      }
      return selected
    }
  })
  return server
}

afterEach(() => {
  destroyable?.destroy()
  tasks.clear()
})

it('checks SyncMap access', async () => {
  let server = getServer()

  let wrong = await server.connect('wrong')
  await server.expectDenied(() => wrong.subscribe('tasks/10'))
  await server.expectDenied(() => wrong.subscribe('tasks'))

  let correct = await server.connect('10')
  await server.expectDenied(() => correct.subscribe('tasks/bad'))
  await server.expectDenied(() => correct.subscribe('tasks', { text: 'A' }))
  await server.expectDenied(() =>
    correct.process(
      createdTask({ fields: { finished: false, text: 'One' }, id: '10' })
    )
  )
  await server.expectDenied(() => correct.process(deletedTask({ id: '10' })))
})

it('supports 404', async () => {
  let server = getServer()
  let client = await server.connect('1')
  await server.expectUndo('notFound', () => client.subscribe('tasks/10'))
})

it('supports SyncMap', async () => {
  let server = getServer()
  let client1 = await server.connect('1')
  let client2 = await server.connect('2')

  client1.log.keepActions()
  client2.log.keepActions()

  await client1.process(
    createTask({ fields: { finished: false, text: 'One' }, id: '10' })
  )
  expect(Object.fromEntries(tasks)).toEqual({
    10: { finished: false, finishedChanged: 1, text: 'One', textChanged: 1 }
  })

  expect(await client1.subscribe('tasks/10')).toEqual([
    changedTask({ fields: { finished: false, text: 'One' }, id: '10' })
  ])
  expect(getTime(client1, changedTask)).toEqual([1])
  await client2.subscribe('tasks/10')

  expect(
    await client2.collect(() =>
      client1.process(changeTask({ fields: { text: 'One1' }, id: '10' }))
    )
  ).toEqual([changedTask({ fields: { text: 'One1' }, id: '10' })])
  expect(Object.fromEntries(tasks)).toEqual({
    10: { finished: false, finishedChanged: 1, text: 'One1', textChanged: 10 }
  })
  expect(getTime(client2, changedTask)).toEqual([1, 10])

  expect(
    await client1.collect(async () => {
      await client1.process(changeTask({ fields: { text: 'One2' }, id: '10' }))
    })
  ).toEqual([loguxProcessed({ id: '13 1:1:1 0' })])

  await client1.process(changeTask({ fields: { text: 'One0' }, id: '10' }), {
    time: 12
  })
  expect(Object.fromEntries(tasks)).toEqual({
    10: { finished: false, finishedChanged: 1, text: 'One2', textChanged: 13 }
  })

  let client3 = await server.connect('3')
  expect(
    await client3.subscribe('tasks/10', undefined, { id: '', time: 12 })
  ).toEqual([changedTask({ fields: { text: 'One2' }, id: '10' })])

  let client4 = await server.connect('3')
  expect(
    await client4.subscribe('tasks/10', undefined, { id: '', time: 20 })
  ).toEqual([])
})

it('supports SyncMap filters', async () => {
  let server = getServer()

  let client1 = await server.connect('1')
  let client2 = await server.connect('2')

  expect(await client1.subscribe('tasks')).toEqual([])
  expect(
    await client1.process(
      createTask({ fields: { finished: false, text: 'One' }, id: '1' })
    )
  ).toEqual([loguxProcessed({ id: '3 1:1:1 0' })])
  await client1.process(
    createTask({ fields: { finished: true, text: 'Two' }, id: '2' })
  )
  await client1.process(
    createTask({ fields: { finished: false, text: 'Three' }, id: '3' })
  )

  expect(await client2.subscribe('tasks', { finished: false })).toEqual([
    loguxSubscribed({ channel: 'tasks/1' }),
    loguxSubscribed({ channel: 'tasks/3' }),
    changedTask({ fields: { finished: false, text: 'One' }, id: '1' }),
    changedTask({ fields: { finished: false, text: 'Three' }, id: '3' })
  ])

  expect(
    await client2.collect(async () => {
      await client1.process(changeTask({ fields: { text: 'One1' }, id: '1' }))
    })
  ).toEqual([changedTask({ fields: { text: 'One1' }, id: '1' })])

  expect(
    await client2.collect(async () => {
      await client1.process(deleteTask({ id: '3' }))
    })
  ).toEqual([deletedTask({ id: '3' })])
  expect(Object.fromEntries(tasks)).toEqual({
    1: { finished: false, finishedChanged: 3, text: 'One1', textChanged: 18 },
    2: { finished: true, finishedChanged: 6, text: 'Two', textChanged: 6 }
  })

  expect(
    await client2.collect(async () => {
      await client1.process(
        createTask({ fields: { finished: false, text: 'Four' }, id: '4' })
      )
    })
  ).toEqual([
    createdTask({ fields: { finished: false, text: 'Four' }, id: '4' })
  ])

  expect(
    await client2.collect(async () => {
      await client1.process(
        createTask({ fields: { finished: true, text: 'Five' }, id: '5' })
      )
    })
  ).toEqual([])

  expect(
    await client2.collect(async () => {
      await client1.process(
        createTask({ fields: { finished: true, text: 'S' }, id: 'silence' })
      )
    })
  ).toEqual([])

  let client3 = await server.connect('3')
  expect(
    await client3.subscribe('tasks', undefined, { id: '', time: 15 })
  ).toEqual([
    loguxSubscribed({ channel: 'tasks/1' }),
    loguxSubscribed({ channel: 'tasks/2' }),
    loguxSubscribed({ channel: 'tasks/4' }),
    loguxSubscribed({ channel: 'tasks/5' }),
    loguxSubscribed({ channel: 'tasks/silence' }),
    changedTask({ fields: { text: 'One1' }, id: '1' }),
    changedTask({ fields: { finished: false, text: 'Four' }, id: '4' }),
    changedTask({ fields: { finished: true, text: 'Five' }, id: '5' }),
    changedTask({ fields: { finished: true, text: 'S' }, id: 'silence' })
  ])

  expect(
    await client3.collect(async () => {
      await client1.process(
        createTask({ fields: { finished: true, text: 'Six' }, id: '6' })
      )
    })
  ).toEqual([createdTask({ fields: { finished: true, text: 'Six' }, id: '6' })])
})

it('supports simpler SyncMap', async () => {
  let server = getServer()
  addSyncMap<CommentValue>(server, 'comments', {
    access() {
      return true
    },
    load(ctx, id, since) {
      if (since) {
        return {
          author: NoConflictResolution('A'),
          id,
          text: NoConflictResolution('updated')
        }
      }
      return {
        author: NoConflictResolution('A'),
        id,
        text: NoConflictResolution('full')
      }
    }
  })
  addSyncMapFilter<CommentValue>(server, 'comments', {
    access() {
      return true
    },
    initial() {
      return []
    }
  })

  let client1 = await server.connect('1')

  expect(await client1.subscribe('comments/1')).toEqual([
    changedComment({ fields: { author: 'A', text: 'full' }, id: '1' })
  ])
  expect(
    await client1.subscribe('comments/2', undefined, { id: '', time: 2 })
  ).toEqual([
    changedComment({ fields: { author: 'A', text: 'updated' }, id: '2' })
  ])

  let client2 = await server.connect('2')
  await client2.subscribe('comments')
  await client2.collect(() =>
    server.process(
      changedComment({ fields: { author: 'A', text: '2' }, id: '10' })
    )
  )
})

it('allows to disable changes', async () => {
  let server = getServer()
  addSyncMap<CommentValue>(server, 'comments', {
    access() {
      return true
    },
    change(ctx, id) {
      return id !== 'bad'
    },
    create(ctx, id) {
      return id !== 'bad'
    },
    delete(ctx, id) {
      return id !== 'bad'
    },
    load(ctx, id) {
      return { id }
    }
  })
  addSyncMapFilter<CommentValue>(server, 'comments', {
    access() {
      return true
    },
    initial() {
      return []
    }
  })

  let client1 = await server.connect('1')
  let client2 = await server.connect('2')

  await client2.subscribe('comments')
  await client2.subscribe('comments/good')
  await client2.subscribe('comments/bad')
  expect(
    await client2.collect(async () => {
      await client1.process(createComment({ fields: {}, id: 'good' }))
      await client1.process(changeComment({ fields: {}, id: 'good' }))
      await client1.process(deleteComment({ id: 'good' }))
      await client1.process(createComment({ fields: {}, id: 'bad' }))
      await client1.process(changeComment({ fields: {}, id: 'bad' }))
      await client1.process(deleteComment({ id: 'bad' }))
    })
  ).toEqual([
    createdComment({ fields: {}, id: 'good' }),
    changedComment({ fields: {}, id: 'good' }),
    deletedComment({ id: 'good' })
  ])
})

it('does not load data on creating', async () => {
  let loaded = 0
  let server = getServer()
  addSyncMap<CommentValue>(server, 'comments', {
    access() {
      return true
    },
    load(ctx, id) {
      loaded += 1
      return { id }
    }
  })

  let client = await server.connect('1')

  await client.log.add({
    channel: 'comments/new',
    type: 'logux/subscribe'
  })
  await setTimeout(10)
  expect(loaded).toBe(1)

  await client.log.add({
    channel: 'comments/new',
    creating: true,
    type: 'logux/subscribe'
  })
  await setTimeout(10)
  expect(loaded).toBe(1)
})

it('throws an error on missed value wrapper', async () => {
  let server = getServer()
  addSyncMap<CommentValue>(server, 'comments', {
    access() {
      return true
    },
    // @ts-expect-error
    load(ctx, id) {
      return { id, text: 'Text' }
    }
  })

  let client = await server.connect('1')

  await server.expectError(/Wrap value/, () => client.subscribe('comments/1'))
})


================================================
FILE: allowed-meta/index.d.ts
================================================
/**
 * List of meta keys permitted for clients.
 *
 *```js
 * import { ALLOWED_META } from '@logux/server'
 * async function onSend (action, meta) {
 *   const filtered = { }
 *   for (const i in meta) {
 *     if (ALLOWED_META.includes(i)) {
 *       filtered[i] = meta[i]
 *     }
 *   }
 *   return [action, filtered]
 * }
 * ```
 */
export const ALLOWED_META: string[]


================================================
FILE: allowed-meta/index.js
================================================
export const ALLOWED_META = ['id', 'time', 'subprotocol']


================================================
FILE: allowed-meta/index.test.ts
================================================
import { expect, it } from 'vitest'

import { ALLOWED_META } from '../index.js'

it('has allowed meta keys list', () => {
  for (let key of ALLOWED_META) {
    expect(typeof key).toEqual('string')
  }
})


================================================
FILE: base-server/index.d.ts
================================================
import type {
  AbstractActionCreator,
  LoguxSubscribeAction,
  LoguxUnsubscribeAction
} from '@logux/actions'
import type {
  Action,
  AnyAction,
  ID,
  Log,
  LogStore,
  Meta,
  ServerConnection,
  TestTime
} from '@logux/core'
import type { Unsubscribe } from 'nanoevents'
import type {
  Server as HTTPServer,
  IncomingMessage,
  ServerResponse
} from 'node:http'
import type { WebSocket } from 'ws'

import type {
  ChannelContext,
  ConnectContext,
  Context
} from '../context/index.js'
import type { ServerClient } from '../server-client/index.js'

interface LogFn {
  (...objs: unknown[]): void
}

interface TypeOptions {
  /**
   * Name of the queue that will be used to process actions
   * of the specified type. Default is 'main'
   */
  queue?: string
}

interface ChannelOptions {
  /**
   * Name of the queue that will be used to process channels
   * with the specified name pattern. Default is 'main'
   */
  queue?: string
}

interface ConnectLoader<Headers extends object = unknown> {
  (
    ctx: ConnectContext<Headers>,
    lastSynced: number
  ):
    | [Action, ServerMeta][]
    | Promise<
        [
          Action,
          Partial<Pick<ServerMeta, 'subprotocol'>> &
            Pick<ServerMeta, 'id' | 'time'>
        ][]
      >
}

type ServerNodeConstructor = new (...args: unknown[]) => ServerNode

export interface ServerMeta extends Meta {
  /**
   * All nodes subscribed to channel will receive the action.
   */
  channel?: string

  /**
   * All nodes subscribed to listed channels will receive the action.
   */
  channels?: string[]

  /**
   * All nodes with listed client ID will receive the action.
   */
  client?: string

  /**
   * All nodes with listed client IDs will receive the action.
   */
  clients?: string[]

  /**
   * Client IDs, which will not receive the action.
   */
  excludeClients?: string[]

  /**
   * Node with listed node ID will receive the action.
   */
  node?: string

  /**
   * All nodes with listed node IDs will receive the action.
   */
  nodes?: string[]

  /**
   * Node ID of the server received the action.
   */
  server: string

  /**
   * Action processing status
   */
  status?: 'error' | 'processed' | 'waiting'

  /**
   * All nodes with listed user ID will receive the action.
   */
  user?: string

  /**
   * All nodes with listed user IDs will receive the action.
   */
  users?: string[]
}

export interface BaseServerOptions {
  /**
   * SSL certificate or path to it. Path could be relative from server
   * root. It is required in production mode, because WSS is highly
   * recommended.
   */
  cert?: string

  /**
   * Regular expression which should be cleaned from error message and stack.
   *
   * By default it cleans `Bearer [^\s"]+`.
   */
  cleanFromLog?: RegExp

  /**
   * Disable health check endpoint, {@link Server#http}.
   *
   * The server will process only WebSocket connection and ignore all other
   * HTTP request (so they can be processed by other HTTP server).
   */
  disableHttpServer?: boolean

  /**
   * Development or production server mode. By default,
   * it will be taken from `NODE_ENV` environment variable.
   * On empty `NODE_ENV` it will be `'development'`.
   */
  env?: 'development' | 'production'

  /**
   * URL of main JS file in the root dir for the cases where you can’t use
   * `import.meta.dirname`.
   *
   * ```
   * fileUrl: import.meta.url
   * ```
   */
  fileUrl?: string

  /**
   * IP-address to bind server. Default is `127.0.0.1`.
   */
  host?: string

  /**
   * Custom random ID to be used in node ID.
   */
  id?: string

  /**
   * SSL key or path to it. Path could be relative from server root.
   * It is required in production mode, because WSS is highly recommended.
   */
  key?: { pem: string } | string

  /**
   * The version requirements for client subprotocol version.
   */
  minSubprotocol?: number

  /**
   * Replace class for ServerNode.
   */
  Node?: ServerNodeConstructor

  /**
   * Process ID, to display in logs.
   */
  pid?: number

  /**
   * Milliseconds since last message to test connection by sending ping.
   * Default is `20000`.
   */
  ping?: number

  /**
   * Port to bind server. It will create HTTP server manually to connect
   * WebSocket server to it. Default is `31337`.
   */
  port?: number | string

  /**
   * URL to Redis for Logux Server Pro scaling.
   */
  redis?: string

  /**
   * Application root to load files and show errors.
   * Default is `process.cwd()`.
   *
   * ```js
   * root: import.meta.dirname
   * ```
   */
  root?: string

  /**
   * HTTP server to serve Logux’s WebSocket and HTTP requests.
   *
   * Logux will remove previous HTTP callbacks. Do not use it with Express.js
   * or other HTTP servers with defined routes.
   */
  server?: HTTPServer

  /**
   * Store to save log. Will be {@link @logux/core:MemoryStore}, by default.
   */
  store?: LogStore

  /**
   * Server current application subprotocol version.
   */
  subprotocol?: number

  /**
   * Test time to test server.
   */
  time?: TestTime

  /**
   * Timeout in milliseconds to disconnect connection.
   * Default is `70000`.
   */
  timeout?: number
}

export interface AuthenticatorOptions<Headers extends object> {
  client: ServerClient
  cookie: Record<string, string>
  headers: Headers
  token: string
  userId: string
}

export type SendBackActions =
  | [Action, Partial<Meta>][]
  | Action
  | Action[]
  | void

/**
 * The authentication callback.
 *
 * @param userId User ID.
 * @param token The client credentials.
 * @param client Client object.
 * @returns `true` if credentials was correct
 */
interface ServerAuthenticator<Headers extends object> {
  (user: AuthenticatorOptions<Headers>): boolean | Promise<boolean>
}

/**
 * Check does user can do this action.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns `true` if client are allowed to use this action.
 */
interface Authorizer<
  TypeAction extends Action,
  Data extends object,
  Headers extends object
> {
  (
    ctx: Context<Data, Headers>,
    action: Readonly<TypeAction>,
    meta: Readonly<ServerMeta>
  ): boolean | Promise<boolean>
}

/**
 * Return object with keys for meta to resend action to other users.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns Meta’s keys.
 */
interface Resender<
  TypeAction extends Action,
  Data extends object,
  Headers extends object
> {
  (
    ctx: Context<Data, Headers>,
    action: Readonly<TypeAction>,
    meta: Readonly<ServerMeta>
  ): Promise<Resend> | Resend
}

/**
 * Action business logic.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns Promise when processing will be finished.
 */
interface Processor<
  TypeAction extends Action,
  Data extends object,
  Headers extends object
> {
  (
    ctx: Context<Data, Headers>,
    action: Readonly<TypeAction>,
    meta: Readonly<ServerMeta>
  ): Promise<void> | void
}

/**
 * Callback which will be run on the end of action/subscription
 * processing or on an error.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 */
interface ActionFinally<
  TypeAction extends Action,
  Data extends object,
  Headers extends object
> {
  (
    ctx: Context<Data, Headers>,
    action: Readonly<TypeAction>,
    meta: Readonly<ServerMeta>
  ): void
}

/**
 * Channel filter callback
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns Should action be sent to client.
 */
interface ChannelFilter<Headers extends object> {
  (
    ctx: Context<unknown, Headers>,
    action: Readonly<Action>,
    meta: Readonly<ServerMeta>
  ): boolean | Promise<boolean>
}

/**
 * Channel authorizer callback
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns `true` if client are allowed to subscribe to this channel.
 */
interface ChannelAuthorizer<
  SubscribeAction extends Action,
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> {
  (
    ctx: ChannelContext<Data, ChannelParams, Headers>,
    action: Readonly<SubscribeAction>,
    meta: Readonly<ServerMeta>
  ): boolean | Promise<boolean>
}

/**
 * Generates custom filter for channel’s actions.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns Actions filter.
 */
interface FilterCreator<
  SubscribeAction extends Action,
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> {
  (
    ctx: ChannelContext<Data, ChannelParams, Headers>,
    action: Readonly<SubscribeAction>,
    meta: Readonly<ServerMeta>
  ): ChannelFilter<Headers> | Promise<ChannelFilter<Headers>> | void
}

/**
 * Send actions with current state.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 * @returns Promise during current actions loading.
 */
interface ChannelLoader<
  SubscribeAction extends Action,
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> {
  (
    ctx: ChannelContext<Data, ChannelParams, Headers>,
    action: Readonly<SubscribeAction>,
    meta: Readonly<ServerMeta>
  ): Promise<SendBackActions> | SendBackActions
}

/**
 * Callback which will be run on the end of subscription
 * processing or on an error.
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 */
interface ChannelFinally<
  SubscribeAction extends Action,
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> {
  (
    ctx: ChannelContext<Data, ChannelParams, Headers>,
    action: Readonly<SubscribeAction>,
    meta: Readonly<ServerMeta>
  ): void
}

/**
 * Callback which will be called on listener unsubscribe
 * (with explicit intent or because of disconnect)
 *
 * @param ctx Information about node, who create this action.
 * @param action The action data.
 * @param meta The action metadata.
 */
interface ChannelUnsubscribe<
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> {
  (
    ctx: ChannelContext<Data, ChannelParams, Headers>,
    action: LoguxUnsubscribeAction,
    meta: Readonly<ServerMeta>
  ): void
}

type ActionCallbacks<
  TypeAction extends Action,
  Data extends object,
  Headers extends object
> = (
  | {
      access: Authorizer<TypeAction, Data, Headers>
      process?: Processor<TypeAction, Data, Headers>
    }
  | {
      accessAndProcess: Processor<TypeAction, Data, Headers>
    }
) & {
  finally?: ActionFinally<TypeAction, Data, Headers>
  resend?: Resender<TypeAction, Data, Headers>
}

type ChannelCallbacks<
  SubscribeAction extends Action,
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> = (
  | {
      access: ChannelAuthorizer<SubscribeAction, Data, ChannelParams, Headers>
      load?: ChannelLoader<SubscribeAction, Data, ChannelParams, Headers>
    }
  | {
      accessAndLoad: ChannelLoader<
        SubscribeAction,
        Data,
        ChannelParams,
        Headers
      >
    }
) & {
  filter?: FilterCreator<SubscribeAction, Data, ChannelParams, Headers>
  finally?: ChannelFinally<SubscribeAction, Data, ChannelParams, Headers>
  unsubscribe?: ChannelUnsubscribe<Data, ChannelParams, Headers>
}

interface ActionReporter {
  action: Readonly<Action>
  meta: Readonly<ServerMeta>
}

interface SubscriptionReporter {
  actionId: ID
  channel: string
}

interface CleanReporter {
  actionId: ID
}

interface AuthenticationReporter {
  connectionId: string
  nodeId: string
  subprotocol: string
}

interface ReportersArguments {
  add: ActionReporter
  addClean: ActionReporter
  authenticated: AuthenticationReporter
  clean: CleanReporter
  clientError: {
    connectionId?: string
    err: Error
    nodeId?: string
  }
  connect: {
    connectionId: string
    ipAddress: string
  }
  denied: CleanReporter
  destroy: void
  disconnect: {
    connectionId?: string
    nodeId?: string
  }
  error: {
    actionId?: ID
    connectionId?: string
    err: Error
    fatal?: true
    nodeId?: string
  }
  listen: {
    cert: boolean
    environment: 'development' | 'production'
    host: string
    loguxServer: string
    minSubprotocol: number
    nodeId: string
    notes: object
    port: string
    redis: string
    server: boolean
    subprotocol: number
  }
  processed: {
    actionId: ID
    latency: number
  }
  subscribed: SubscriptionReporter
  unauthenticated: AuthenticationReporter
  unknownType: {
    actionId: ID
    type: string
  }
  unsubscribed: SubscriptionReporter
  useless: ActionReporter
  wrongChannel: SubscriptionReporter
  zombie: {
    nodeId: string
  }
}

export interface Reporter {
  <Event extends keyof ReportersArguments>(
    event: Event,
    payload: ReportersArguments[Event]
  ): void
}

export type Resend =
  | {
      channel?: string
      channels?: string[]
      client?: string
      clients?: string[]
      excludeClients?: string[]
      node?: string
      nodes?: string[]
      user?: string
      users?: string[]
    }
  | string
  | string[]

export interface Logger {
  debug(details: object, message: string): void
  error(details: object, message: string): void
  fatal(details: object, message: string): void
  info(details: object, message: string): void
  warn(details: object, message: string): void
}

/**
 * Return `false` if `cb()` got response error with 403.
 *
 * ```js
 * import { wasNot403 } from '@logux/server'
 *
 * server.auth(({ userId, token }) => {
 *   return wasNot403(async () => {
 *     get(`/checkUser/${userId}/${token}`)
 *   })
 * })
 * ```
 *
 * @param cb Callback with `request` calls.
 */
export function wasNot403(cb: () => Promise<void>): Promise<boolean>

/**
 * Base server class to extend.
 */
export class BaseServer<
  Headers extends object = unknown,
  ServerLog extends Log = Log<ServerMeta>
> {
  /**
   * Connected client by client ID.
   *
   * Do not rely on this data, when you have multiple Logux servers.
   * Each server will have a different list.
   */
  clientIds: Map<string, ServerClient>

  /**
   * Connected clients.
   *
   * ```js
   * for (let client of server.connected.values()) {
   *   console.log(client.remoteAddress)
   * }
   * ```
   */
  connected: Map<string, ServerClient>

  /**
   * Production or development mode.
   *
   * ```js
   * if (server.env === 'development') {
   *   logDebugData()
   * }
   * ```
   */
  env: 'development' | 'production'

  /**
   * Server actions log.
   *
   * ```js
   * server.log.each(finder)
   * ```
   */
  log: ServerLog

  /**
   * Console for custom log records. It uses `pino` API.
   *
   * ```js
   * server.on('connected', client => {
   *   server.logger.info(
   *     { domain: client.httpHeaders.domain },
   *     'Client domain'
   *   )
   * })
   * ```
   */
  logger: {
    debug: LogFn
    error: LogFn
    fatal: LogFn
    info: LogFn
    warn: LogFn
  }

  /**
   * Server unique ID.
   *
   * ```js
   * console.log('Error was raised on ' + server.nodeId)
   * ```
   */
  nodeId: string

  /**
   * Connected client by node ID.
   *
   * Do not rely on this data, when you have multiple Logux servers.
   * Each server will have a different list.
   */
  nodeIds: Map<string, ServerClient>

  /**
   * Server options.
   *
   * ```js
   * console.log('Server options', server.options.subprotocol)
   * ```
   */
  options: BaseServerOptions

  /**
   * Clients subscribed to some channel.
   *
   * Do not rely on this data, when you have multiple Logux servers.
   * Each server will have a different list.
   */
  subscribers: {
    [channel: string]: {
      [nodeId: string]: {
        filters: Record<string, ChannelFilter<unknown> | true>
        unsubscribe?: (action: LoguxUnsubscribeAction, meta: ServerMeta) => void
      }
    }
  }

  /**
   * Connected client by user ID.
   *
   * Do not rely on this data, when you have multiple Logux servers.
   * Each server will have a different list.
   */
  userIds: Map<string, ServerClient[]>

  /**
   * @param opts Server options.
   */
  constructor(opts: BaseServerOptions)

  /**
   * Add new client for server. You should call this method manually
   * mostly for test purposes.
   *
   * ```js
   * server.addClient(test.right)
   * ```
   *
   * @param connection Logux connection to client.
   * @returns Client ID.
   */
  addClient(connection: ServerConnection): number

  /**
   * Set authenticate function. It will receive client credentials
   * and node ID. It should return a Promise with `true` or `false`.
   *
   * ```js
   * server.auth(async ({ userId, cookie }) => {
   *   const user = await findUserByToken(cookie.token)
   *   return !!user && userId === user.id
   * })
   * ```
   *
   * @param authenticator The authentication callback.
   */
  auth(authenticator: ServerAuthenticator<Headers>): void

  /**
   * Define the channel.
   *
   * ```js
   * server.channel('user/:id', {
   *   access (ctx, action, meta) {
   *     return ctx.params.id === ctx.userId
   *   }
   *   filter (ctx, action, meta) {
   *     return (otherCtx, otherAction, otherMeta) => {
   *       return !action.hidden
   *     }
   *   }
   *   async load (ctx, action, meta) {
   *     const user = await db.loadUser(ctx.params.id)
   *     ctx.sendBack({ type: 'USER_NAME', name: user.name })
   *   }
   * })
   * ```
   *
   * @param pattern Pattern for channel name.
   * @param callbacks Callback during subscription process.
   * @param options Additional options
   */
  channel<
    ChannelParams extends object = unknown,
    Data extends object = unknown,
    SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction
  >(
    pattern: string,
    callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>,
    options?: ChannelOptions
  ): void
  /**
   * @param pattern Regular expression for channel name.
   * @param callbacks Callback during subscription process.
   * @param options Additional options
   */
  channel<
    ChannelParams extends string[] = string[],
    Data extends object = unknown,
    SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction
  >(
    pattern: RegExp,
    callbacks: ChannelCallbacks<SubscribeAction, Data, ChannelParams, Headers>,
    options?: ChannelOptions
  ): void

  /**
   * Send runtime error stacktrace to all clients.
   *
   * ```js
   * process.on('uncaughtException', e => {
   *   server.debugError(e)
   * })
   * ```
   *
   * @param error Runtime error instance.
   */
  debugError(error: Error): void

  /**
   * Stop server and unbind all listeners.
   *
   * ```js
   * afterEach(() => {
   *   testServer.destroy()
   * })
   * ```
   *
   * @returns Promise when all listeners will be removed.
   */
  destroy(): Promise<void>

  /**
   * Handle WebSocket connection explicitly
   *
   * This is a low-level method allowing to integrate Logux server with an existing server
   *
   * ```js
   * fastify.get('/', { websocket: true }, (socket, req) => {
   *   loguxServer.handleClient(socket, req)
   * })
   * ```
   */
  handleClient(ws: WebSocket, req: IncomingMessage): void

  /**
   * Add non-WebSocket HTTP request processor.
   *
   * ```js
   * server.http('GET', '/auth', (req, res) => {
   *   let token = signIn(req)
   *   if (token) {
   *     res.setHeader('Set-Cookie', `token=${token}; Secure; HttpOnly`)
   *     res.end()
   *   } else {
   *     res.statusCode = 400
   *     res.end('Wrong user or password')
   *   }
   * })
   * ```
   */
  http(
    method: string,
    url: string,
    listener: (
      req: IncomingMessage,
      res: ServerResponse
    ) => Promise<void> | void
  ): void
  http(
    listener: (
      req: IncomingMessage,
      res: ServerResponse
    ) => boolean | Promise<boolean>
  ): void

  /**
   * Start WebSocket server and listen for clients.
   *
   * @returns When the server has been bound.
   */
  listen(): Promise<void>

  /**
   * @param event The event name.
   * @param listener Event listener.
   */
  on(event: 'subscriptionCancelled', listener: () => void): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Subscription listener.
   */
  on(
    event: 'subscribing',
    listener: (action: LoguxSubscribeAction, meta: Readonly<ServerMeta>) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Processing listener.
   */
  on(
    event: 'processed',
    listener: (
      action: Action,
      meta: Readonly<ServerMeta>,
      latencyMilliseconds: number
    ) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Action listener.
   */
  on(
    event: 'add' | 'clean',
    listener: (action: Action, meta: Readonly<ServerMeta>) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Client listener.
   */
  on(
    event: 'connected' | 'disconnected',
    listener: (client: ServerClient) => void
  ): Unsubscribe
  /**
   * Subscribe for synchronization events. It implements nanoevents API.
   * Supported events:
   *
   * * `error`: server error during action processing.
   * * `fatal`: server error during loading.
   * * `clientError`: wrong client behaviour.
   * * `connected`: new client was connected.
   * * `disconnected`: client was disconnected.
   * * `authenticated`: client was authenticated.
   * * `preadd`: action is going to be added to the log.
   *   The best place to set `reasons`.
   * * `add`: action was added to the log.
   * * `clean`: action was cleaned from the log.
   * * `processed`: action processing was finished.
   * * `subscribed`: channel initial data was loaded.
   * * `subscribing`: channel initial data started to be loaded.
   * * `unsubscribed`: node was unsubscribed.
   * * `subscriptionCancelled`: subscription was cancelled because the client
   *    is not connected.
   *
   * ```js
   * server.on('error', error => {
   *   trackError(error)
   * })
   * ```
   *
   * @param event The event name.
   * @param listener The listener function.
   * @returns Unbind listener from event.
   */
  on(
    event: 'clientError' | 'fatal',
    listener: (err: Error) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Error listener.
   */
  on(
    event: 'error',
    listener: (err: Error, action: Action, meta: Readonly<ServerMeta>) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Client listener.
   */
  on(
    event: 'authenticated' | 'unauthenticated',
    listener: (client: ServerClient, latencyMilliseconds: number) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Action listener.
   */
  on(
    event: 'preadd',
    listener: (action: Action, meta: ServerMeta) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Subscription listener.
   */
  on(
    event: 'subscribed',
    listener: (
      action: LoguxSubscribeAction,
      meta: Readonly<ServerMeta>,
      latencyMilliseconds: number
    ) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Subscription listener.
   */
  on(
    event: 'unsubscribed',
    listener: (
      action: LoguxUnsubscribeAction,
      meta: Readonly<ServerMeta>,
      clientNodeId: string
    ) => void
  ): Unsubscribe
  /**
   * @param event The event name.
   * @param listener Report listener.
   */
  on(event: 'report', listener: Reporter): Unsubscribe

  /**
   * Set callbacks for unknown channel subscription.
   *
   *```js
   * server.otherChannel({
   *   async access (ctx, action, meta) {
   *     const res = await phpBackend.checkChannel(ctx.params[0], ctx.userId)
   *     if (res.code === 404) {
   *       this.wrongChannel(action, meta)
   *       return false
   *     } else {
   *       return response.body === 'granted'
   *     }
   *   }
   * })
   * ```
   *
   * @param callbacks Callback during subscription process.
   */
  otherChannel<Data extends object = unknown>(
    callbacks: ChannelCallbacks<LoguxSubscribeAction, Data, [string], Headers>
  ): void

  /**
   * Define callbacks for actions, which type was not defined
   * by any {@link Server#type}. Useful for proxy or some hacks.
   *
   * Without this settings, server will call {@link Server#unknownType}
   * on unknown type.
   *
   * ```js
   * server.otherType(
   *   async access (ctx, action, meta) {
   *     const response = await phpBackend.checkByHTTP(action, meta)
   *     if (response.code === 404) {
   *       this.unknownType(action, meta)
   *       return false
   *     } else {
   *       return response.body === 'granted'
   *     }
   *   }
   *   async process (ctx, action, meta) {
   *     return await phpBackend.sendHTTP(action, meta)
   *   }
   * })
   * ```
   *
   * @param callbacks Callbacks for actions with this type.
   */
  otherType<Data extends object = unknown>(
    callbacks: ActionCallbacks<Action, Data, Headers>
  ): void

  /**
   * Add new action to the server and return the Promise until it will be
   * resend to clients and processed.
   *
   * @param action New action to resend and process.
   * @param meta Action’s meta.
   * @returns Promise until new action will be resend to clients and processed.
   */
  process(
    action: AnyAction,
    meta?: Partial<ServerMeta>
  ): Promise<Readonly<ServerMeta>>

  /**
   * Send action, received by other server, to all clients of current server.
   * This method is for multi-server configuration only.
   *
   * ```js
   * server.on('add', (action, meta) => {
   *   if (meta.server === server.nodeId) {
   *     sendToOtherServers(action, meta)
   *   }
   * })
   * onReceivingFromOtherServer((action, meta) => {
   *   server.sendAction(action, meta)
   * })
   * ```
   *
   * @param action New action.
   * @param meta Action’s metadata.
   */
  sendAction(action: Action, meta: ServerMeta): Promise<void> | void

  /**
   * Change a way how server loads actions history for the client.
   *
   * ```js
   * server.sendOnConnect(async (ctx, lastSynced) => {
   *   return db.loadActions({ user: ctx.userId, after: lastSynced })
   * })
   * ```
   *
   * @param loader Callback which loads list of actions and meta.
   */
  sendOnConnect(loader: ConnectLoader<Headers>): void

  /**
   * Send `logux/subscribed` if client was not already subscribed.
   *
   * ```js
   * server.subscribe(ctx.nodeId, `users/${loaded}`)
   * ```
   *
   * @param nodeId Node ID.
   * @param channel Channel name.
   */
  subscribe(nodeId: string, channel: string): void

  /**
   * @param actionCreator Action creator function.
   * @param callbacks Callbacks for action created by creator.
   * @param options Additional options
   */
  type<Creator extends AbstractActionCreator, Data extends object = unknown>(
    actionCreator: Creator,
    callbacks: ActionCallbacks<ReturnType<Creator>, Data, Headers>,
    options?: TypeOptions
  ): void
  /**
   * Define action type’s callbacks.
   *
   * ```js
   * server.type('CHANGE_NAME', {
   *   access (ctx, action, meta) {
   *     return action.user === ctx.userId
   *   },
   *   resend (ctx, action) {
   *     return `user/${ action.user }`
   *   }
   *   process (ctx, action, meta) {
   *     if (isFirstOlder(lastNameChange(action.user), meta)) {
   *       return db.changeUserName({ id: action.user, name: action.name })
   *     }
   *   }
   * })
   * ```
   *
   * @param name The action’s type or action’s type matching rule as RegExp..
   * @param callbacks Callbacks for actions with this type.
   * @param options Additional options
   */
  type<TypeAction extends Action = AnyAction, Data extends object = unknown>(
    name: RegExp | TypeAction['type'],
    callbacks: ActionCallbacks<TypeAction, Data, Headers>,
    options?: TypeOptions
  ): void

  /**
   * Undo action from client.
   *
   * ```js
   * if (couldNotFixConflict(action, meta)) {
   *   server.undo(action, meta)
   * }
   * ```
   *
   * @param action The original action to undo.
   * @param meta The action’s metadata.
   * @param reason Optional code for reason. Default is `'error'`.
   * @param extra Extra fields to `logux/undo` action.
   * @returns When action was saved to the log.
   */
  undo(
    action: Action,
    meta: ServerMeta,
    reason?: string,
    extra?: object
  ): Promise<void>

  /**
   * If you receive action with unknown type, this method will mark this action
   * with `error` status and undo it on the clients.
   *
   * If you didn’t set {@link Server#otherType},
   * Logux will call it automatically.
   *
   * ```js
   * server.otherType({
   *   access (ctx, action, meta) {
   *     if (action.type.startsWith('myapp/')) {
   *       return proxy.access(action, meta)
   *     } else {
   *       server.unknownType(action, meta)
   *     }
   *   }
   * })
   * ```
   *
   * @param action The action with unknown type.
   * @param meta Action’s metadata.
   */
  unknownType(action: Action, meta: ServerMeta): void

  /**
   * Report that client try to subscribe for unknown channel.
   *
   * Logux call it automatically,
   * if you will not set {@link Server#otherChannel}.
   *
   * ```js
   * server.otherChannel({
   *   async access (ctx, action, meta) {
   *     const res = phpBackend.checkChannel(params[0], ctx.userId)
   *     if (res.code === 404) {
   *       this.wrongChannel(action, meta)
   *       return false
   *     } else {
   *       return response.body === 'granted'
   *     }
   *   }
   * })
   * ```
   *
   * @param action The subscribe action.
   * @param meta Action’s metadata.
   */
  wrongChannel(action: LoguxSubscribeAction, meta: ServerMeta): void
}


================================================
FILE: base-server/index.js
================================================
import { LoguxNotFoundError } from '@logux/actions'
import { Log, MemoryStore, parseId, ServerConnection } from '@logux/core'
import { createNanoEvents } from 'nanoevents'
import { nanoid } from 'nanoid'
import { readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import UrlPattern from 'url-pattern'
import { WebSocketServer } from 'ws'

import { addHttpPages } from '../add-http-pages/index.js'
import { Context } from '../context/index.js'
import { createHttpServer } from '../create-http-server/index.js'
import { ServerClient } from '../server-client/index.js'

const SKIP_PROCESS = Symbol('skipProcess')
const RESEND_META = ['channels', 'users', 'clients', 'nodes']

function optionError(msg) {
  let error = new Error(msg)
  error.logux = true
  error.note = 'Check server constructor and Logux Server documentation'
  throw error
}

export async function wasNot403(cb) {
  try {
    await cb()
    return true
  } catch (e) {
    if (e.name === 'ResponseError' && e.statusCode === 403) {
      return false
    }
    throw e
  }
}

function normalizeTypeCallbacks(name, callbacks) {
  if (callbacks && callbacks.accessAndProcess) {
    callbacks.access = (ctx, ...args) => {
      return wasNot403(async () => {
        await callbacks.accessAndProcess(ctx, ...args)
        ctx[SKIP_PROCESS] = true
      })
    }
    callbacks.process = async (ctx, ...args) => {
      if (!ctx[SKIP_PROCESS]) await callbacks.accessAndProcess(ctx, ...args)
    }
  }
  if (!callbacks || !callbacks.access) {
    throw new Error(`${name} must have access callback`)
  }
}

function normalizeChannelCallbacks(pattern, callbacks) {
  if (callbacks && callbacks.accessAndLoad) {
    callbacks.access = (ctx, ...args) => {
      return wasNot403(async () => {
        try {
          ctx.data.load = await callbacks.accessAndLoad(ctx, ...args)
        } catch (e) {
          if (e.name === 'LoguxNotFoundError') {
            ctx.data.notFound = true
          } else if (e.name === 'ResponseError' && e.statusCode === 404) {
            ctx.data.notFound = true
          } else {
            throw e
          }
        }
      })
    }
    callbacks.load = ctx => {
      if (ctx.data.notFound) {
        throw new LoguxNotFoundError()
      } else {
        return ctx.data.load
      }
    }
  }
  if (!callbacks || !callbacks.access) {
    throw new Error(`Channel ${pattern} must have access callback`)
  }
}

function subscriberFilterId(action) {
  return JSON.stringify(action.filter || {})
}

export class BaseServer {
  constructor(opts = {}) {
    this.options = opts
    this.env = this.options.env || process.env.NODE_ENV || 'development'

    if (typeof this.options.subprotocol === 'undefined') {
      throw optionError('Missed `subprotocol` option in server constructor')
    }
    if (typeof this.options.minSubprotocol === 'undefined') {
      throw optionError('Missed `minSubprotocol` option in server constructor')
    }

    if (this.options.key && !this.options.cert) {
      throw optionError('You must set `cert` option if you use `key` option')
    }
    if (!this.options.key && this.options.cert) {
      throw optionError('You must set `key` option if you use `cert` option')
    }

    if (!this.options.server) {
      if (!this.options.port) this.options.port = 31337
      if (!this.options.host) this.options.host = '127.0.0.1'
    }

    this.nodeId = `server:${this.options.id || nanoid(8)}`

    if (this.options.fileUrl) {
      this.options.root = dirname(fileURLToPath(this.options.fileUrl))
    }

    this.options.root = this.options.root || process.cwd()
    if (typeof this.options.port === 'string') {
      this.options.port = parseInt(this.options.port, 10)
    }

    let store = this.options.store || new MemoryStore()

    let log
    if (this.options.time) {
      log = this.options.time.nextLog({ nodeId: this.nodeId, store })
    } else {
      log = new Log({ nodeId: this.nodeId, store })
    }

    this.logger = console

    this.contexts = new WeakMap()
    this.log = log

    let cleaned = {}

    this.on('preadd', (action, meta) => {
      let isLogux = action.type.slice(0, 6) === 'logux/'
      if (!meta.server) {
        meta.server = this.nodeId
      }
      if (!meta.status && !isLogux) {
        meta.status = 'waiting'
      }
      if (meta.id.split(' ')[1] === this.nodeId) {
        if (!meta.subprotocol) {
          meta.subprotocol = this.options.subprotocol
        }
        if (
          !isLogux &&
          !this.types[action.type] &&
          !this.getRegexProcessor(action.type)
        ) {
          meta.status = 'processed'
        }
      }
      this.replaceResendShortcuts(meta)
    })
    this.on('add', async (action, meta) => {
      let start = Date.now()
      if (meta.reasons.length === 0) {
        cleaned[meta.id] = true
        this.emitter.emit('report', 'addClean', { action, meta })
      } else {
        this.emitter.emit('report', 'add', { action, meta })
      }

      if (this.destroying && !this.actionToQueue.has(meta.id)) {
        return
      }

      if (action.type === 'logux/subscribe') {
        if (meta.server === this.nodeId) {
          void this.subscribeAction(action, meta, start)
        }
        return
      }

      if (action.type === 'logux/unsubscribe') {
        if (meta.server === this.nodeId) {
          this.unsubscribeAction(action, meta)
        }
        return
      }

      let processor = this.getProcessor(action.type)
      if (processor && processor.resend && meta.status === 'waiting') {
        let ctx = this.createContext(action, meta)
        let resend
        try {
          resend = await processor.resend(ctx, action, meta)
        } catch (e) {
          this.undo(action, meta, 'error')
          this.emitter.emit('error', e, action, meta)
          this.finally(processor, ctx, action, meta)
          return
        }
        if (resend) {
          if (typeof resend === 'string') {
            resend = { channels: [resend] }
          } else if (Array.isArray(resend)) {
            resend = { channels: resend }
          } else {
            this.replaceResendShortcuts(resend)
          }
          let diff = {}
          for (let i of RESEND_META) {
            if (resend[i]) diff[i] = resend[i]
          }
          await this.log.changeMeta(meta.id, diff)
          meta = { ...meta, ...diff }
        }
      }

      if (this.isUseless(action, meta)) {
        this.emitter.emit('report', 'useless', { action, meta })
      }

      await this.sendAction(action, meta)

      if (meta.status === 'waiting') {
        if (!processor) {
          this.internalUnknownType(action, meta)
          return
        }
        if (processor.process) {
          void this.processAction(processor, action, meta, start)
        } else {
          this.emitter.emit('processed', action, meta, 0)
          this.finally(
            processor,
            this.createContext(action, meta),
            action,
            meta
          )
          this.markAsProcessed(meta)
        }
      } else {
        this.emitter.emit('processed', action, meta, 0)
        this.finally(processor, this.createContext(action, meta), action, meta)
      }
    })
    this.on('clean', (action, meta) => {
      if (cleaned[meta.id]) {
        delete cleaned[meta.id]
        return
      }
      this.emitter.emit('report', 'clean', { actionId: meta.id })
    })

    this.emitter = createNanoEvents()
    this.on('fatal', err => {
      this.emitter.emit('report', 'error', { err, fatal: true })
    })
    this.on('error', (err, action, meta) => {
      if (meta) {
        this.emitter.emit('report', 'error', { actionId: meta.id, err })
      } else if (err.nodeId) {
        this.emitter.emit('report', 'error', { err, nodeId: err.nodeId })
      } else if (err.connectionId) {
        this.emitter.emit('report', 'error', {
          connectionId: err.connectionId,
          err
        })
      }
      if (this.env === 'development') this.debugError(err)
    })
    this.on('clientError', err => {
      if (err.nodeId) {
        this.emitter.emit('report', 'clientError', { err, nodeId: err.nodeId })
      } else if (err.connectionId) {
        this.emitter.emit('report', 'clientError', {
          connectionId: err.connectionId,
          err
        })
      }
    })
    this.on('connected', client => {
      this.emitter.emit('report', 'connect', {
        connectionId: client.key,
        ipAddress: client.remoteAddress
      })
    })
    this.on('disconnected', client => {
      if (!client.zombie) {
        if (client.nodeId) {
          this.emitter.emit('report', 'disconnect', { nodeId: client.nodeId })
        } else {
          this.emitter.emit('report', 'disconnect', {
            connectionId: client.key
          })
        }
      }
    })

    this.unbind = []

    this.connected = new Map()
    this.nodeIds = new Map()
    this.clientIds = new Map()
    this.userIds = new Map()
    this.types = {}
    this.regexTypes = new Map()
    this.processing = 0

    this.lastClient = 0

    this.channels = []
    this.subscribers = {}

    this.authAttempts = {}
    this.unknownTypes = {}
    this.wrongChannels = {}

    this.timeouts = {}
    this.lastTimeout = 0

    this.typeToQueue = new Map()
    this.queues = new Map()
    this.actionToQueue = new Map()

    this.httpListeners = {}
    this.httpAllListeners = []
    addHttpPages(this)

    this.listenNotes = {}

    let end = (actionId, queue, queueKey, ...args) => {
      this.actionToQueue.delete(actionId)
      if (queue.length() === 0) {
        this.queues.delete(queueKey)
      }
      queue.next(...args)
    }
    let undoRemainingTasks = queue => {
      let remainingTasks = queue.getQueue()
      if (remainingTasks) {
        for (let task of remainingTasks) {
          this.undo(task.action, task.meta, 'error')
          this.actionToQueue.delete(task.meta.id)
        }
      }
      queue.killAndDrain()
    }
    this.on('error', (e, action, meta) => {
      let queueKey = this.actionToQueue.get(meta?.id)
      if (queueKey) {
        let queue = this.queues.get(queueKey)
        undoRemainingTasks(queue)
        end(meta.id, queue, queueKey, e)
      }
    })
    this.on('processed', (action, meta) => {
      if (action.type === 'logux/undo') {
        let queueKey = this.actionToQueue.get(action.id)
        if (queueKey) {
          let queue = this.queues.get(queueKey)
          undoRemainingTasks(queue)
          end(action.id, queue, queueKey, null, meta)
        }
      } else if (action.type === 'logux/processed') {
        let queueKey = this.actionToQueue.get(action.id)
        if (queueKey) {
          let queue = this.queues.get(queueKey)
          end(action.id, queue, queueKey, null, meta)
        }
      } else if (
        action.type !== 'logux/subscribed' &&
        action.type !== 'logux/unsubscribed'
      ) {
        let queueKey = this.actionToQueue.get(meta.id)
        if (queueKey) {
          let queue = this.queues.get(queueKey)
          end(meta.id, queue, queueKey, null, meta)
        }
      }
    })

    this.unbind.push(() => {
      for (let i of this.connected.values()) i.destroy()
      for (let i in this.timeouts) {
        clearTimeout(this.timeouts[i])
      }
    })
    this.unbind.push(() => {
      return new Promise(resolve => {
        if (this.processing === 0) {
          resolve()
        } else {
          this.on('processed', () => {
            if (this.processing === 0) resolve()
          })
        }
      })
    })
    this.unbind.push(() => {
      return Promise.allSettled(
        [...this.queues.values()].map(queue => {
          return new Promise(resolve => {
            queue.drain = resolve
          })
        })
      )
    })
  }

  addClient(connection) {
    this.lastClient += 1
    let key = this.lastClient.toString()
    let client = new ServerClient(this, connection, key)
    this.connected.set(key, client)
    return this.lastClient
  }

  auth(authenticator) {
    this.authenticator = authenticator
  }

  buildUndo(action, meta, reason, extra) {
    let undoMeta = { status: 'processed' }

    if (meta.users) undoMeta.users = meta.users.slice(0)
    if (meta.nodes) undoMeta.nodes = meta.nodes.slice(0)
    if (meta.clients) undoMeta.clients = meta.clients.slice(0)
    if (meta.reasons) undoMeta.reasons = meta.reasons.slice(0)
    if (meta.channels) undoMeta.channels = meta.channels.slice(0)
    if (meta.excludeClients) {
      undoMeta.excludeClients = meta.excludeClients.slice(0)
    }

    let undoAction = {
      ...extra,
      action,
      id: meta.id,
      reason,
      type: 'logux/undo'
    }
    return [undoAction, undoMeta]
  }

  channel(pattern, callbacks, options = {}) {
    normalizeChannelCallbacks(`Channel ${pattern}`, callbacks)
    let channel = Object.assign({}, callbacks)
    if (typeof pattern === 'string') {
      channel.pattern = new UrlPattern(pattern, {
        segmentValueCharset: '^/'
      })
    } else {
      channel.regexp = pattern
    }

    channel.queueName = options.queue || 'main'
    this.channels.push(channel)
  }

  createContext(action, meta) {
    let context = this.contexts.get(action)
    if (!context) {
      context = new Context(this, meta)
      this.contexts.set(action, context)
    }
    return context
  }

  debugActionError(meta, msg) {
    if (this.env === 'development') {
      let clientId = parseId(meta.id).clientId
      if (this.clientIds.has(clientId)) {
        this.clientIds.get(clientId).connection.send(['debug', 'error', msg])
      }
    }
  }

  debugError(error) {
    for (let i of this.connected.values()) {
      if (i.connection.connected) {
        try {
          i.connection.send(['debug', 'error', error.stack])
        } catch {}
      }
    }
  }

  denyAction(action, meta) {
    this.emitter.emit('report', 'denied', { actionId: meta.id })
    this.undo(action, meta, 'denied')
    this.debugActionError(meta, `Action "${meta.id}" was denied`)
  }

  destroy() {
    this.destroying = true
    this.emitter.emit('report', 'destroy')
    return Promise.all(this.unbind.map(i => i()))
  }

  finally(processor, ctx, action, meta) {
    this.contexts.delete(action)
    if (processor && processor.finally) {
      try {
        processor.finally(ctx, action, meta)
      } catch (err) {
        this.emitter.emit('error', err, action, meta)
      }
    }
  }

  getProcessor(type) {
    return (
      this.types[type] || this.getRegexProcessor(type) || this.otherProcessor
    )
  }

  getRegexProcessor(type) {
    for (let regexp of this.regexTypes.keys()) {
      if (type.match(regexp) !== null) {
        return this.regexTypes.get(regexp)
      }
    }
    return undefined
  }

  handleClient(ws, req) {
    ws.upgradeReq = req
    this.addClient(new ServerConnection(ws))
  }

  http(method, url, listener) {
    if (this.options.disableHttpServer) {
      throw new Error(
        '`server.http()` can not be called when `disableHttpServer` enabled'
      )
    }
    if (!url) {
      this.httpAllListeners.push(method)
    } else {
      this.httpListeners[`${method} ${url}`] = listener
    }
  }

  internalUnknownType(action, meta) {
    this.contexts.delete(action)
    this.log.changeMeta(meta.id, { status: 'error' })
    this.emitter.emit('report', 'unknownType', {
      actionId: meta.id,
      type: action.type
    })
    if (parseId(meta.id).userId !== 'server') {
      this.undo(action, meta, 'unknownType')
    }
    this.debugActionError(meta, `Action with unknown type ${action.type}`)
  }

  internalWrongChannel(action, meta) {
    this.contexts.delete(action)
    this.emitter.emit('report', 'wrongChannel', {
      actionId: meta.id,
      channel: action.channel
    })
    this.undo(action, meta, 'wrongChannel')
    this.debugActionError(meta, `Wrong channel name ${action.channel}`)
  }

  isBruteforce(ip) {
    let attempts = this.authAttempts[ip]
    return attempts && attempts >= 3
  }

  isUseless(action, meta) {
    if (
      meta.status !== 'processed' ||
      this.types[action.type] ||
      this.getRegexProcessor(action.type)
    ) {
      return false
    }
    for (let i of ['channels', 'nodes', 'clients', 'users']) {
      if (Array.isArray(meta[i]) && meta[i].length > 0) return false
    }
    return true
  }

  async listen() {
    if (!this.authenticator) {
      throw new Error('You must set authentication callback by server.auth()')
    }
    this.httpServer = await createHttpServer(this.options)
    this.ws = new WebSocketServer({ server: this.httpServer })
    if (!this.options.server) {
      await new Promise((resolve, reject) => {
        this.ws.on('error', reject)
        this.httpServer.listen(this.options.port, this.options.host, resolve)
      })
    }

    let processing = 0
    let waiting
    this.unbind.push(() => {
      return new Promise(resolve => {
        let end = () => {
          this.ws.close(resolve)
          this.httpServer.close()
        }
        if (processing === 0) {
          end()
        } else {
          waiting = end
        }
      })
    })

    if (!this.options.disableHttpServer) {
      this.httpServer.on('request', async (req, res) => {
        if (this.destroying) {
          res.writeHead(503, { 'Content-Type': 'text/plain' })
          res.end('The server is shutting down\n')
          return
        }
        processing += 1
        await this.processHttp(req, res)
        processing -= 1
        if (processing === 0 && waiting) waiting()
      })
    }

    let pkg = JSON.parse(
      await readFile(join(import.meta.dirname, '..', 'package.json'))
    )

    this.ws.on('connection', (ws, req) => this.handleClient(ws, req))
    this.emitter.emit('report', 'listen', {
      cert: !!this.options.cert,
      environment: this.env,
      host: this.options.host,
      loguxServer: pkg.version,
      minSubprotocol: this.options.minSubprotocol,
      nodeId: this.nodeId,
      notes: this.listenNotes,
      port: this.options.port,
      redis: this.options.redis,
      server: !!this.options.server,
      subprotocol: this.options.subprotocol
    })
  }

  markAsProcessed(meta) {
    this.log.changeMeta(meta.id, { status: 'processed' })
    let data = parseId(meta.id)
    if (data.userId !== 'server') {
      this.log.add(
        { id: meta.id, type: 'logux/processed' },
        { clients: [data.clientId], status: 'processed' }
      )
    }
  }

  on(event, listener) {
    if (event === 'preadd' || event === 'add' || event === 'clean') {
      return this.log.emitter.on(event, listener)
    } else {
      return this.emitter.on(event, listener)
    }
  }

  otherChannel(callbacks) {
    normalizeChannelCallbacks('Unknown channel', callbacks)
    if (this.otherSubscriber) {
      throw new Error('Callbacks for unknown channel are already defined')
    }
    let channel = Object.assign({}, callbacks)
    channel.pattern = {
      match(name) {
        return [name]
      }
    }
    this.otherSubscriber = channel
  }

  otherType(callbacks) {
    if (this.otherProcessor) {
      throw new Error('Callbacks for unknown types are already defined')
    }
    normalizeTypeCallbacks('Unknown type', callbacks)
    this.otherProcessor = callbacks
  }

  performUnsubscribe(clientNodeId, action, meta) {
    if (action.channel === '__proto__' || clientNodeId === '__proto__') return
    if (this.subscribers[action.channel]) {
      let subscriber = this.subscribers[action.channel][clientNodeId]
      if (subscriber) {
        if (subscriber.unsubscribe) {
          subscriber.unsubscribe(action, meta)
          this.contexts.delete(action)
        }
        let filterId = subscriberFilterId(action)
        delete subscriber.filters[filterId]
        if (Object.keys(subscriber.filters).length === 0) {
          delete this.subscribers[action.channel][clientNodeId]
        }
        if (Object.keys(this.subscribers[action.channel]).length === 0) {
          delete this.subscribers[action.channel]
        }
      }
    }
    this.emitter.emit('unsubscribed', action, meta, clientNodeId)
    this.emitter.emit('report', 'unsubscribed', {
      actionId: meta.id,
      channel: action.channel
    })
  }

  process(action, meta = {}) {
    return new Promise((resolve, reject) => {
      let unbindError = this.on('error', (e, errorAction) => {
        if (errorAction === action) {
          unbindError()
          unbindProcessed()
          reject(e)
        }
      })
      let unbindProcessed = this.on('processed', (processed, processedMeta) => {
        if (processed === action) {
          unbindError()
          unbindProcessed()
          resolve(processedMeta)
        }
      })
      this.log.add(action, meta)
    })
  }

  async processAction(processor, action, meta, start) {
    let ctx = this.createContext(action, meta)

    let latency
    this.processing += 1
    try {
      await processor.process(ctx, action, meta)
      latency = Date.now() - start
      this.markAsProcessed(meta)
    } catch (e) {
      this.log.changeMeta(meta.id, { status: 'error' })
      this.undo(action, meta, 'error')
      this.emitter.emit('error', e, action, meta)
    } finally {
      this.finally(processor, ctx, action, meta)
    }
    if (typeof latency === 'undefined') latency = Date.now() - start
    this.processing -= 1
    this.emitter.emit('processed', action, meta, latency)
  }

  async processHttp(req, res) {
    let urlString = req.url
    if (/^\/\w+%3F/.test(urlString)) {
      urlString = decodeURIComponent(urlString)
    }
    let reqUrl = new URL(urlString, 'http://localhost')
    let rule = this.httpListeners[req.method + ' ' + reqUrl.pathname]

    if (!rule) {
      let processed = false
      for (let listener of this.httpAllListeners) {
        let result = await listener(req, res)
        if (result === true) {
          processed = true
          break
        }
      }
      if (!processed) {
        res.writeHead(404, { 'Content-Type': 'text/plain' })
        res.end('Not found\n')
      }
    } else {
      await rule(req, res)
    }
  }

  rememberBadAuth(ip) {
    this.authAttempts[ip] = (this.authAttempts[ip] || 0) + 1
    this.setTimeout(() => {
      if (this.authAttempts[ip] === 1) {
        delete this.authAttempts[ip]
      } else {
        this.authAttempts[ip] -= 1
      }
    }, 3000)
  }

  replaceResendShortcuts(meta) {
    if (meta.channel) {
      meta.channels = [meta.channel]
      delete meta.channel
    }
    if (meta.user) {
      meta.users = [meta.user]
      delete meta.user
    }
    if (meta.client) {
      meta.clients = [meta.client]
      delete meta.client
    }
    if (meta.node) {
      meta.nodes = [meta.node]
      delete meta.node
    }
  }

  async sendAction(action, meta) {
    let from = parseId(meta.id).clientId
    let ignoreClients = new Set(meta.excludeClients || [])
    ignoreClients.add(from)

    if (meta.nodes) {
      for (let id of meta.nodes) {
        let client = this.nodeIds.get(id)
        if (client) {
          ignoreClients.add(client.clientId)
          client.node.onAdd(action, meta)
        }
      }
    }

    if (meta.clients) {
      for (let id of meta.clients) {
        if (this.clientIds.has(id)) {
          let client = this.clientIds.get(id)
          ignoreClients.add(client.clientId)
          client.node.onAdd(action, meta)
        }
      }
    }

    if (meta.users) {
      for (let userId of meta.users) {
        let users = this.userIds.get(userId)
        if (users) {
          for (let client of users) {
            if (!ignoreClients.has(client.clientId)) {
              ignoreClients.add(client.clientId)
              client.node.onAdd(action, meta)
            }
          }
        }
      }
    }

    if (meta.channels) {
      for (let channel of meta.channels) {
        if (this.subscribers[channel]) {
          for (let nodeId in this.subscribers[channel]) {
            let clientId = parseId(nodeId).clientId
            if (!ignoreClients.has(clientId)) {
              let subscriber = this.subscribers[channel][nodeId]
              if (subscriber) {
                let ctx = this.createContext(action, meta)
                let client = this.clientIds.get(clientId)
                for (let filter of Object.values(subscriber.filters)) {
                  filter =
                    typeof filter === 'function'
                      ? await filter(ctx, action, meta)
                      : filter
                  if (filter && client) {
                    ignoreClients.add(clientId)
                    client.node.onAdd(action, meta)
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  sendOnConnect(loader) {
    this.connectLoader = loader
  }

  setTimeout(callback, ms) {
    this.lastTimeout += 1
    let id = this.lastTimeout
    this.timeouts[id] = setTimeout(() => {
      delete this.timeouts[id]
      callback()
    }, ms)
  }

  subscribe(nodeId, channel) {
    if (channel === '__proto__' || nodeId === '__proto__') return
    if (!this.subscribers[channel] || !this.subscribers[channel][nodeId]) {
      if (!this.subscribers[channel]) {
        this.subscribers[channel] = {}
      }
      this.subscribers[channel][nodeId] = { filters: { '{}': true } }
      this.log.add({ channel, type: 'logux/subscribed' }, { nodes: [nodeId] })
    }
  }

  async subscribeAction(action, meta, start) {
    if (typeof action.channel !== 'string' || action.channel === '__proto__') {
      this.wrongChannel(action, meta)
      return
    }

    let channels = this.channels
    if (this.otherSubscriber) {
      channels = this.channels.concat([this.otherSubscriber])
    }

    let match
    for (let channel of channels) {
      if (channel.pattern) {
        match = channel.pattern.match(action.channel)
      } else {
        match = action.channel.match(channel.regexp)
      }

      let subscribed = false
      if (match) {
        let ctx = this.createContext(action, meta)
        if (ctx.nodeId === '__proto__') return
        ctx.params = match
        try {
          let access = await channel.access(ctx, action, meta)
          if (this.wrongChannels[meta.id]) {
            delete this.wrongChannels[meta.id]
            return
          }
          if (!access) {
            this.denyAction(action, meta)
            return
          }

          let client = this.clientIds.get(ctx.clientId)
          if (!client) {
            this.emitter.emit('subscriptionCancelled')
            return
          }

          let filterId = subscriberFilterId(action)
          let filters = { [filterId]: true }

          if (channel.filter) {
            let filter = await channel.filter(ctx, action, meta)
            filters = { [filterId]: filter }
          }

          this.emitter.emit('report', 'subscribed', {
            actionId: meta.id,
            channel: action.channel
          })

          if (!this.subscribers[action.channel]) {
            this.subscribers[action.channel] = {}
            this.emitter.emit('subscribing', action, meta)
          }
          let subscriber = this.subscribers[action.channel][ctx.nodeId]
          if (subscriber) {
            filters = { ...subscriber.filters, ...filters }
          }
          this.subscribers[action.channel][ctx.nodeId] = {
            filters,
            unsubscribe: channel.unsubscribe
              ? (unsubscribeAction, unsubscribeMeta) =>
                  channel.unsubscribe(ctx, unsubscribeAction, unsubscribeMeta)
              : undefined
          }
          subscribed = true

          if (channel.load) {
            let sendBack = await channel.load(ctx, action, meta)
            if (Array.isArray(sendBack)) {
              await Promise.all(
                sendBack.map(i => {
                  return Array.isArray(i) ? ctx.sendBack(...i) : ctx.sendBack(i)
                })
              )
            } else if (sendBack) {
              await ctx.sendBack(sendBack)
            }
          }
          this.emitter.emit('subscribed', action, meta, Date.now() - start)
          this.markAsProcessed(meta)
        } catch (e) {
          if (e.name === 'LoguxNotFoundError') {
            this.undo(action, meta, 'notFound')
          } else {
            this.emitter.emit('error', e, action, meta)
            this.undo(action, meta, 'error')
          }
          if (subscribed) {
            this.unsubscribe(action, meta)
          }
        } finally {
          this.finally(channel, ctx, action, meta)
        }
        break
      }
    }

    if (!match) this.wrongChannel(action, meta)
  }

  type(name, callbacks, options = {}) {
    let queue = options.queue || 'main'
    this.typeToQueue.set(name, queue)

    if (typeof name === 'function') name = name.type
    normalizeTypeCallbacks(`Action type ${name}`, callbacks)

    if (name instanceof RegExp) {
      this.regexTypes.set(name, callbacks)
    } else {
      if (this.types[name]) {
        throw new Error(`Action type ${name} was already defined`)
      }
      this.types[name] = callbacks
    }
  }

  undo(action, meta, reason = 'error', extra = {}) {
    let clientId = parseId(meta.id).clientId
    let [undoAction, undoMeta] = this.buildUndo(action, meta, reason, extra)
    undoMeta.clients = (undoMeta.clients || []).concat([clientId])
    return this.log.add(undoAction, undoMeta)
  }

  unknownType(action, meta) {
    this.internalUnknownType(action, meta)
    this.unknownTypes[meta.id] = true
  }

  unsubscribe(action, meta) {
    let clientNodeId = meta.id.split(' ')[1]
    this.performUnsubscribe(clientNodeId, action, meta)
  }

  unsubscribeAction(action, meta) {
    if (typeof action.channel !== 'string') {
      this.wrongChannel(action, meta)
      return
    }

    this.unsubscribe(action, meta)

    this.markAsProcessed(meta)
    this.contexts.delete(action)
  }

  wrongChannel(action, meta) {
    this.internalWrongChannel(action, meta)
    this.wrongChannels[meta.id] = true
  }
}


================================================
FILE: base-server/index.test.ts
================================================
import { defineAction } from '@logux/actions'
import {
  type Action,
  Log,
  MemoryStore,
  type TestLog,
  TestTime
} from '@logux/core'
import { restoreAll, spy, type Spy, spyOn } from 'nanospy'
import { readFileSync } from 'node:fs'
import http from 'node:http'
import https from 'node:https'
import { join } from 'node:path'
import { setTimeout } from 'node:timers/promises'
import { afterEach, expect, it } from 'vitest'
import WebSocket from 'ws'

import {
  BaseServer,
  type BaseServerOptions,
  type ServerMeta
} from '../index.js'

const ROOT = join(import.meta.dirname, '..')

const DEFAULT_OPTIONS = {
  minSubprotocol: 0,
  subprotocol: 0
}
const CERT = join(ROOT, 'test/fixtures/cert.pem')
const KEY = join(ROOT, 'test/fixtures/key.pem')

let lastPort = 9111

function createServer(
  options: Partial<BaseServerOptions> = {}
): BaseServer<object, TestLog<ServerMeta>> {
  let opts = {
    ...DEFAULT_OPTIONS,
    ...options
  }
  if (typeof opts.time === 'undefined') {
    opts.time = new TestTime()
    opts.id = 'uuid'
  }
  if (typeof opts.port === 'undefined') {
    lastPort += 1
    opts.port = lastPort
  }

  let created = new BaseServer<object, TestLog<ServerMeta>>(opts)
  created.auth(() => true)

  destroyable = created

  return created
}

let destroyable: BaseServer | undefined
let httpServer: http.Server | undefined

function createReporter(opts: Partial<BaseServerOptions> = {}): {
  app: BaseServer<object, TestLog<ServerMeta>>
  names: string[]
  reports: [string, any][]
} {
  let names: string[] = []
  let reports: [string, any][] = []

  let app = createServer(opts)
  app.on('report', (name: string, details?: any) => {
    names.push(name)
    if (details?.meta) {
      details.meta = JSON.parse(JSON.stringify(details.meta))
    }
    reports.push([name, details])
  })
  return { app, names, reports }
}

let originEnv = process.env.NODE_ENV

function privateMethods(obj: object): any {
  return obj
}

function emit(obj: any, event: string, ...args: any): void {
  obj.emitter.emit(event, ...args)
}

async function catchError(cb: () => Promise<any>): Promise<Error> {
  try {
    await cb()
  } catch (e) {
    if (e instanceof Error) return e
  }
  throw new Error('Error was not thrown')
}

function calls(fn: Function | undefined): any[][] {
  return (fn as any as Spy).calls
}

function called(fn: Function | undefined): boolean {
  return (fn as any as Spy).called
}

function callCount(fn: Function | undefined): number {
  return (fn as any as Spy).callCount
}

afterEach(async () => {
  restoreAll()
  process.env.NODE_ENV = originEnv
  if (destroyable) {
    await destroyable.destroy()
    destroyable = undefined
  }
  if (httpServer) httpServer.close()
})

it('saves server options', () => {
  let app = new BaseServer({
    minSubprotocol: 1,
    subprotocol: 1
  })
  expect(app.options.minSubprotocol).toEqual(1)
})

it('generates node ID', () => {
  let app = new BaseServer({
    minSubprotocol: 1,
    subprotocol: 1
  })
  expect(app.nodeId).toMatch(/^server:[\w-]+$/)
})

it('throws on missed subprotocol', () => {
  expect(() => {
    new BaseServer({})
  }).toThrow(/Missed `subprotocol` option/)
})

it('throws on missed supported subprotocols', () => {
  expect(() => {
    new BaseServer({ subprotocol: 0 })
  }).toThrow(/Missed `minSubprotocol` option/)
})

it('sets development environment by default', () => {
  delete process.env.NODE_ENV
  let app = new BaseServer(DEFAULT_OPTIONS)
  expect(app.env).toEqual('development')
})

it('takes environment from NODE_ENV', () => {
  process.env.NODE_ENV = 'production'
  let app = new BaseServer(DEFAULT_OPTIONS)
  expect(app.env).toEqual('production')
})

it('sets environment from user', () => {
  let app = new BaseServer({
    env: 'production',
    minSubprotocol: 0,
    subprotocol: 0
  })
  expect(app.env).toEqual('production')
})

it('uses cwd as default root', () => {
  let app = new BaseServer(DEFAULT_OPTIONS)
  expect(app.options.root).toEqual(process.cwd())
})

it('supports string as port', () => {
  let app = new BaseServer({ ...DEFAULT_OPTIONS, port: '8080' })
  expect(app.options.port).toEqual(8080)
})

it('uses user root', () => {
  let app = new BaseServer({
    minSubprotocol: 0,
    root: '/a',
    subprotocol: 0
  })
  expect(app.options.root).toEqual('/a')
})

it('creates log with default store', () => {
  let app = new BaseServer(DEFAULT_OPTIONS)
  expect(app.log instanceof Log).toBe(true)
  expect(app.log.store instanceof MemoryStore).toBe(true)
})

it('creates log with custom store', () => {
  let store = new MemoryStore()
  let app = new BaseServer({
    minSubprotocol: 0,
    store,
    subprotocol: 0
  })
  expect(app.log.store).toBe(store)
})

it('uses test time and ID', () => {
  let store = new MemoryStore()
  let app = new BaseServer({
    id: 'uuid',
    minSubprotocol: 0,
    store,
    subprotocol: 0,
    time: new TestTime()
  })
  expect(app.log.store).toEqual(store)
  expect(app.log.generateId()).toEqual('1 server:uuid 0')
})

it('destroys application without runned server', async () => {
  let app = new BaseServer(DEFAULT_OPTIONS)
  await app.destroy()
  app.destroy()
})

it('throws without authenticator', async () => {
  expect.assertions(1)
  let app = new BaseServer(DEFAULT_OPTIONS)
  let error = await catchError(() => app.listen())
  expect(error.message).toMatch(/authentication/)
})

it('sets default ports and hosts', () => {
  let app = createServer()
  expect(app.options.port).toEqual(31337)
  expect(app.options.host).toEqual('127.0.0.1')
})

it('uses user port', () => {
  let app = createServer({ port: 1337 })
  expect(app.options.port).toEqual(1337)
})

it('throws a error on key without certificate', () => {
  expect(() => {
    createServer({ key: readFileSync(KEY).toString() })
  }).toThrow(/set `cert` option/)
})

it('throws a error on certificate without key', () => {
  expect(() => {
    createServer({ cert: readFileSync(CERT).toString() })
  }).toThrow(/set `key` option/)
})

it('uses HTTPS', async () => {
  let app = createServer({
    cert: readFileSync(CERT).toString(),
    key: readFileSync(KEY).toString()
  })
  await app.listen()
  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)
})

it('loads keys by absolute path', async () => {
  let app = createServer({
    cert: CERT,
    key: KEY
  })
  await app.listen()
  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)
})

it('loads keys by relative path', async () => {
  let app = createServer({
    cert: 'fixtures/cert.pem',
    key: 'fixtures/key.pem',
    root: join(ROOT, 'test/')
  })
  await app.listen()
  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)
})

it('supports object in SSL key', async () => {
  let app = createServer({
    cert: readFileSync(CERT).toString(),
    key: { pem: readFileSync(KEY).toString() }
  })
  await app.listen()
  expect(privateMethods(app).httpServer instanceof https.Server).toBe(true)
})

it('reporters on start listening', async () => {
  let test = createReporter({
    redis: '//localhost'
  })
  let pkgFile = readFileSync(join(ROOT, 'package.json'))
  let pkg = JSON.parse(pkgFile.toString())

  privateMethods(test.app).listenNotes.prometheus =
    'http://127.0.0.1:31338/prometheus'

  let promise = test.app.listen()
  expect(test.reports).toEqual([])

  await promise
  expect(test.reports).toEqual([
    [
      'listen',
      {
        cert: false,
        environment: 'test',
        host: '127.0.0.1',
        loguxServer: pkg.version,
        minSubprotocol: 0,
        nodeId: 'server:uuid',
        notes: {
          prometheus: 'http://127.0.0.1:31338/prometheus'
        },
        port: test.app.options.port,
        redis: '//localhost',
        server: false,
        subprotocol: 0
      }
    ]
  ])
})

it('reporters on log events', async () => {
  let test = createReporter()
  test.app.type('A', { access: () => true })
  test.app.type('B', { access: () => true })
  await test.app.log.add({ type: 'A' }, { reasons: ['some'] })
  await test.app.log.add({ type: 'B' })
  await test.app.log.removeReason('some')
  expect(test.reports).toEqual([
    [
      'add',
      {
        action: {
          type: 'A'
        },
        meta: {
          added: 1,
          id: '1 server:uuid 0',
          reasons: ['some'],
          server: 'server:uuid',
          status: 'waiting',
          subprotocol: 0,
          time: 1
        }
      }
    ],
    [
      'addClean',
      {
        action: {
          type: 'B'
        },
        meta: {
          id: '2 server:uuid 0',
          reasons: [],
          server: 'server:uuid',
          status: 'waiting',
          subprotocol: 0,
          time: 2
        }
      }
    ],
    [
      'clean',
      {
        actionId: '1 server:uuid 0'
      }
    ]
  ])
})

it('reporters on destroying', () => {
  let test = createReporter()
  let promise = test.app.destroy()
  expect(test.reports).toEqual([['destroy', undefined]])
  return promise
})

it('creates a client on connection', async () => {
  let app = createServer()
  await app.listen()
  let ws = new WebSocket(`ws://127.0.0.1:${app.options.port}`)
  await new Promise((resolve, reject) => {
    ws.onopen = resolve
    ws.onerror = reject
  })
  expect(app.connected.size).toEqual(1)
  expect(app.connected.get('1')?.remoteAddress).toEqual('127.0.0.1')
})

it('creates a client manually', () => {
  let app = createServer()
  app.addClient({
    on: () => {
      return () => true
    },
    ws: {
      _socket: {
        remoteAddress: '127.0.0.1'
      },
      upgradeReq: {
        headers: {}
      }
    }
  } as any)
  expect(app.connected.size).toEqual(1)
  expect(app.connected.get('1')?.remoteAddress).toEqual('127.0.0.1')
})

it('sends debug message to clients on runtimeError', () => {
  let app = createServer()
  app.connected.set('1', {
    connection: {
      connected: true,
      send: spy()
    },
    destroy: () => false
  } as any)
  app.connected.set('2', {
    connection: {
      connected: false,
      send: spy()
    },
    destroy: () => false
  } as any)
  app.connected.set('3', {
    connection: {
      connected: true,
      send: () => {
        throw new Error()
      }
    },
    destroy: () => false
  } as any)

  let error = new Error('Test Error')
  error.stack = `${error.stack?.split('\n')[0]}\nfake stacktrace`

  app.debugError(error)
  expect(calls(app.connected.get('1')?.connection.send)).toEqual([
    [['debug', 'error', 'Error: Test Error\nfake stacktrace']]
  ])
  expect(called(app.connected.get('2')?.connection.send)).toBe(false)
})

it('disconnects client on destroy', () => {
  let app = createServer()
  app.connected.set('1', { destroy: spy() } as any)
  app.destroy()
  expect(callCount(app.connected.get('1')?.destroy)).toEqual(1)
})

it('accepts custom HTTP server', async () => {
  httpServer = http.createServer()
  let app = createServer({ server: httpServer })

  await new Promise<void>(resolve => {
    httpServer?.listen(app.options.port, resolve)
  })
  await app.listen()

  let ws = new WebSocket(`ws://localhost:${app.options.port}`)
  await new Promise((resolve, reject) => {
    ws.onopen = resolve
    ws.onerror = reject
  })
  expect(app.connected.size).toEqual(1)
})

it('marks actions with own node ID', async () => {
  let app = createServer()
  app.type('A', { access: () => true })

  let servers: string[] = []
  app.on('add', (action, meta) => {
    servers.push(meta.server)
  })

  await app.log.add({ type: 'A' })
  await app.log.add({ type: 'A' }, { server: 'server2' })
  expect(servers).toEqual([app.nodeId, 'server2'])
})

it('marks actions with waiting status', async () => {
  let app = createServer()
  app.type('A', { access: () => true })
  app.channel('a', { access: () => true })

  let statuses: (string | undefined)[] = []
  app.on('add', (action, meta) => {
    statuses.push(meta.status)
  })

  await app.log.add({ type: 'A' })
  await app.log.add({ type: 'A' }, { status: 'processed' })
  await app.log.add({ channel: 'a', type: 'logux/subscribe' })
  expect(statuses).toEqual(['waiting', 'processed', undefined])
})

it('defines actions types', () => {
  let app = createServer()
  app.type('FOO', { access: () => true })
  expect(privateMethods(app).types.FOO).not.toBeUndefined()
})

it('does not allow to define type twice', () => {
  let app = createServer()
  app.type('FOO', { access: () => true })
  expect(() => {
    app.type('FOO', { access: () => true })
  }).toThrow(/already/)
})

it('requires access callback for type', () => {
  let app = createServer()
  expect(() => {
    // @ts-expect-error
    app.type('FOO')
  }).toThrow(/access callback/)
})

it('reports about unknown action type', async () => {
  let test = createReporter()
  await test.app.log.add({ type: 'UNKNOWN' }, { id: '1 10:uuid 0' })
  expect(test.names).toEqual(['addClean', 'unknownType', 'addClean'])
  expect(test.reports[1]).toEqual([
    'unknownType',
    {
      actionId: '1 10:uuid 0',
      type: 'UNKNOWN'
    }
  ])
})

it('ignores unknown type for processed actions', async () => {
  let test = createReporter()
  await test.app.log.add(
    { type: 'A' },
    { channels: ['a'], status: 'processed' }
  )
  expect(test.names).toEqual(['addClean'])
})

it('reports about fatal error', () => {
  let test = createReporter({ env: 'development' })

  let err = new Error('Test')
  emit(test.app, 'fatal', err)

  expect(test.reports).toEqual([['error', { err, fatal: true }]])
})

it('sends errors to clients in development', () => {
  let test = createReporter({ env: 'development' })
  test.app.connected.set('0', {
    connection: { connected: true, send: spy() },
    destroy: () => false
  } as any)

  let err = new Error('Test')
  err.stack = 'stack'
  privateMethods(err).nodeId = '10:uuid'
  emit(test.app, 'error', err)

  expect(test.reports).toEqual([['error', { err, nodeId: '10:uuid' }]])
  expect(calls(test.app.connected.get('0')?.connection.send)).toEqual([
    [['debug', 'error', 'stack']]
  ])
})

it('does not send errors in non-development mode', () => {
  let app = createServer({ env: 'production' })
  app.connected.set('0', {
    connection: { send: spy() },
    destroy: () => false
  } as any)
  emit(app, 'error', new Error('Test'))
  expect(called(app.connected.get('0')?.connection.send)).toBe(false)
})

it('processes actions', async () => {
  let test = createReporter()
  let processed: Action[] = []
  let fired: Action[] = []

  test.app.type('FOO', {
    access: () => true,
    async process(ctx, action, meta) {
      expect(meta.added).toEqual(1)
      expect(ctx.isServer).toBe(true)
      await setTimeout(25)
      processed.push(action)
    }
  })
  test.app.on('processed', (action, meta, latency) => {
    expect(typeof latency).toEqual('number')
    expect(meta.added).toEqual(1)
    fired.push(action)
  })

  await test.app.log.add({ type: 'FOO' }, { reasons: ['test'] })
  expect(fired).toEqual([])
  expect(test.app.log.entries()[0][1].status).toEqual('waiting')
  await setTimeout(30)
  expect(test.app.log.entries()[0][1].status).toEqual('processed')
  expect(processed).toEqual([{ type: 'FOO' }])
  expect(fired).toEqual([{ type: 'FOO' }])
})

it('processes regex matching action', async () => {
  let test = createReporter()
  let processed: Action[] = []
  let fired: Action[] = []

  test.app.type(/.*TODO$/, {
    access: () => true,
    async process(ctx, action, meta) {
      expect(meta.added).toEqual(1)
      expect(ctx.isServer).toBe(true)
      await setTimeout(25)
      processed.push(action)
    }
  })
  test.app.on('processed', (action, meta, latency) => {
    expect(typeof latency).toEqual('number')
    expect(meta.added).toEqual(1)
    fired.push(action)
  })

  await test.app.log.add({ type: 'ADD_TODO' }, { reasons: ['test'] })
  expect(fired).toEqual([])
  expect(test.app.log.entries()[0][1].status).toEqual('waiting')
  await setTimeout(30)
  expect(test.app.log.entries()[0][1].status).toEqual('processed')
  expect(processed).toEqual([{ type: 'ADD_TODO' }])
  expect(fired).toEqual([{ type: 'ADD_TODO' }])
})

it('has full events API', () => {
  let app = createServer()

  let events = 0
  let unbind = app.on('processed', () => {
    events += 1
  })

  emit(app, 'processed', { type: 'FOO' }, { id: '1 1:1 0' })
  emit(app, 'processed', { type: 'FOO' }, { id: '1 1:1 0' })
  unbind()
  emit(app, 'processed', { type: 'FOO' }, { id: '1 1:1 0' })

  expect(events).toEqual(2)
})

it('waits for last processing before destroy', async () => {
  let app = createServer()

  let started = 0
  let process: (() => void) | undefined

  app.type('FOO', {
    access: () => true,
    process() {
      started += 1
      return new Promise(resolve => {
        process = resolve
      })
    }
  })

  let destroyed = false
  await app.log.add({ type: 'FOO' })
  app.destroy().then(() => {
    destroyed = true
  })
  await setTimeout(1)

  expect(destroyed).toBe(false)
  expect(privateMethods(app).processing).toEqual(1)
  await app.log.add({ type: 'FOO' })

  expect(started).toEqual(1)
  if (typeof process === 'undefined') throw new Error('process is not set')
  process()
  await setTimeout(1)

  expect(destroyed).toBe(true)
})

it('reports about error during action processing', async () => {
  let test = createReporter()

  let err = new Error('Test')
  test.app.type('FOO', {
    access: () => true,
    process() {
      throw err
    }
  })

  await test.app.log.add({ type: 'FOO' }, { reasons: ['test'] })
  await setTimeout(1)

  expect(test.names).toEqual(['add', 'error', 'add'])
  expect(test.reports[1]).toEqual([
    'error',
    {
      actionId: '1 server:uuid 0',
      err
    }
  ])
  expect(test.reports[2][1].action).toEqual({
    action: { type: 'FOO' },
    id: '1 server:uuid 0',
    reason: 'error',
    type: 'logux/undo'
  })
})

it('undoes actions on client', async () => {
  let app = createServer()
  app.undo(
    { type: 'FOO' },
    {
      added: 1,
      channels: ['user/1'],
      clients: ['2:client'],
      excludeClients: ['3:client'],
      id: '1 1:client:uuid 0',
      nodes: ['2:client:uuid'],
      reasons: ['user/1/lastValue'],
      server: 'server:uuid',
      time: 1,
      users: ['3']
    },
    'magic',
    {
      one: 1
    }
  )

  expect(app.log.entries()).toEqual([
    [
      {
        action: {
          type: 'FOO'
        },
        id: '1 1:client:uuid 0',
        one: 1,
        reason: 'magic',
        type: 'logux/undo'
      },
      {
        added: 1,
        channels: ['user/1'],
        clients: ['2:client', '1:client'],
        excludeClients: ['3:client'],
        id: '1 server:uuid 0',
        nodes: ['2:client:uuid'],
        reasons: ['user/1/lastValue'],
        server: 'server:uuid',
        status: 'processed',
        subprotocol: 0,
        time: 1,
        users: ['3']
      }
    ]
  ])
})

it('adds current subprotocol to meta', async () => {
  let app = createServer({ subprotocol: 1 })
  app.type('A', { access: () => true })
  await app.log.add({ type: 'A' }, { reasons: ['test'] })
  expect(app.log.entries()[0][1].subprotocol).toEqual(1)
})

it('adds current subprotocol only to own actions', async () => {
  let app = createServer({ subprotocol: 1 })
  app.type('A', { access: () => true })
  await app.log.add({ type: 'A' }, { id: '1 0:other 0', reasons: ['test'] })
  expect(app.log.entries()[0][1].subprotocol).toBeUndefined()
})

it('allows to override subprotocol in meta', async () => {
  let app = createServer({ subprotocol: 2 })
  app.type('A', { access: () => true })
  await app.log.add({ type: 'A' }, { reasons: ['test'], subprotocol: 1 })
  expect(app.log.entries()[0][1].subprotocol).toEqual(1)
})

it('checks channel definition', () => {
  let app = createServer()

  expect(() => {
    // @ts-expect-error
    app.channel('foo/:id')
  }).toThrow('Channel foo/:id must have access callback')

  expect(() => {
    // @ts-expect-error
    app.channel(/^foo:/, { load: true })
  }).toThrow('Channel /^foo:/ must have access callback')
})

it('reports about wrong channel name', async () => {
  let test = createReporter({ env: 'development' })
  test.app.channel('foo', { access: () => true })
  let client: any = {
    connection: { send: spy() },
    node: { onAdd() {} }
  }
  test.app.nodeIds.set('10:uuid', client)
  test.app.clientIds.set('10:uuid', client)
  await test.app.log.add({ type: 'logux/subscribe' }, { id: '1 10:uuid 0' })
  expect(test.names).toEqual(['addClean', 'wrongChannel', 'addClean'])
  expect(test.reports[1][1]).toEqual({
    actionId: '1 10:uuid 0',
    channel: undefined
  })
  expect(test.reports[2][1].action).toEqual({
    action: { type: 'logux/subscribe' },
    id: '1 10:uuid 0',
    reason: 'wrongChannel',
    type: 'logux/undo'
  })
  expect(calls(client.connection.send)).toEqual([
    [['debug', 'error', 'Wrong channel name undefined']]
  ])
  await test.app.log.add({ type: 'logux/unsubscribe' })

  expect(test.reports[4]).toEqual([
    'wrongChannel',
    {
      actionId: '2 server:uuid 0',
      channel: undefined
    }
  ])
  await test.app.log.add({ channel: 'unknown', type: 'logux/subscribe' })

  expect(test.reports[7]).toEqual([
    'wrongChannel',
    {
      actionId: '4 server:uuid 0',
      channel: 'unknown'
    }
  ])
})

it('checks custom channel name subscriber', () => {
  let app = createServer()

  expect(() => {
    // @ts-expect-error
    app.otherChannel()
  }).toThrow('Unknown channel must have access callback')

  app.otherChannel({ access: () => true })
  expect(() => {
    app.otherChannel({ access: () => true })
  }).toThrow('Callbacks for unknown channel are already defined')
})

it('allows to have custom channel name check', async () => {
  let test = createReporter()
  let channels: string[] = []
  test.app.otherChannel({
    access(ctx, action, meta) {
      channels.push(ctx.params[0])
      test.app.wrongChannel(action, meta)
      return false
    }
  })
  let client: any = {
    connection: { send() {} },
    node: { onAdd() {} }
  }
  test.app.nodeIds.set('10:uuid', client)
  test.app.clientIds.set('10:uuid', client)
  await test.app.log.add({ channel: 'foo', type: 'logux/subscribe' })
  expect(channels).toEqual(['foo'])
  expect(test.names).toEqual(['addClean', 'wrongChannel', 'addClean'])
})

it('ignores subscription for other servers', async () => {
  let test = createReporter()
  let action = { type: 'logux/subscribe' }
  await test.app.log.add(action, { server: 'server:other' })
  expect(test.names).toEqual(['addClean'])
})

it('checks channel access', async () => {
  let test = createReporter()
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  test.app.nodeIds.set('10:uuid', client)
  test.app.clientIds.set('10:uuid', client)

  let finalled = 0

  test.app.channel(/^user\/(\d+)$/, {
    async access(ctx) {
      expect(ctx.params[1]).toEqual('10')
      return false
    },
    finally() {
      finalled += 1
    }
  })

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: '1 10:uuid 0' }
  )
  await setTimeout(1)

  expect(test.names).toEqual(['addClean', 'denied', 'addClean'])
  expect(test.reports[1][1]).toEqual({ actionId: '1 10:uuid 0' })
  expect(test.reports[2][1].action).toEqual({
    action: { channel: 'user/10', type: 'logux/subscribe' },
    id: '1 10:uuid 0',
    reason: 'denied',
    type: 'logux/undo'
  })
  expect(test.app.subscribers).toEqual({})
  expect(finalled).toEqual(1)
})

it('reports about errors during channel authorization', async () => {
  let test = createReporter()
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  test.app.nodeIds.set('10:uuid', client)
  test.app.clientIds.set('10:uuid', client)

  let err = new Error()
  test.app.channel(/^user\/(\d+)$/, {
    access() {
      throw err
    }
  })

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: '1 10:uuid 0' }
  )
  await Promise.resolve()
  await Promise.resolve()

  expect(test.names).toEqual(['addClean', 'error', 'addClean'])
  expect(test.reports[1][1]).toEqual({ actionId: '1 10:uuid 0', err })
  expect(test.reports[2][1].action).toEqual({
    action: { channel: 'user/10', type: 'logux/subscribe' },
    id: '1 10:uuid 0',
    reason: 'error',
    type: 'logux/undo'
  })
  expect(test.app.subscribers).toEqual({})
})

it('subscribes clients', async () => {
  let test = createReporter()
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  test.app.nodeIds.set('10:a:uuid', client)
  test.app.clientIds.set('10:a', client)

  let userSubsriptions = 0
  test.app.channel<{ id: string }>('user/:id', {
    access(ctx, action, meta) {
      expect(ctx.params.id).toEqual('10')
      expect(action.channel).toEqual('user/10')
      expect(meta.id).toEqual('1 10:a:uuid 0')
      expect(ctx.nodeId).toEqual('10:a:uuid')
      userSubsriptions += 1
      return true
    }
  })

  let filter = (): boolean => false
  test.app.channel('posts', {
    access() {
      return true
    },
    async filter() {
      return filter
    }
  })

  let events = 0
  test.app.on('subscribed', (action, meta, latency) => {
    expect(action.type).toEqual('logux/subscribe')
    expect(meta.id).toContain('10:a:uuid')
    expect(latency).toBeCloseTo(25, -2)
    events += 1
  })

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: '1 10:a:uuid 0' }
  )
  await setTimeout(1)
  expect(events).toEqual(1)
  expect(userSubsriptions).toEqual(1)
  expect(test.names).toEqual(['addClean', 'subscribed', 'addClean'])
  expect(test.reports[1][1]).toEqual({
    actionId: '1 10:a:uuid 0',
    channel: 'user/10'
  })
  expect(test.reports[2][1].action).toEqual({
    id: '1 10:a:uuid 0',
    type: 'logux/processed'
  })
  expect(test.reports[2][1].meta.clients).toEqual(['10:a'])
  expect(test.reports[2][1].meta.status).toEqual('processed')
  expect(test.app.subscribers).toEqual({
    'user/10': {
      '10:a:uuid': { filters: { '{}': true } }
    }
  })
  await test.app.log.add(
    { channel: 'posts', type: 'logux/subscribe' },
    { id: '2 10:a:uuid 0' }
  )
  await setTimeout(1)

  expect(events).toEqual(2)
  expect(test.app.subscribers).toEqual({
    'posts': {
      '10:a:uuid': { filters: { '{}': filter } }
    },
    'user/10': {
      '10:a:uuid': { filters: { '{}': true } }
    }
  })
  await test.app.log.add(
    { channel: 'user/10', type: 'logux/unsubscribe' },
    { id: '3 10:a:uuid 0' }
  )

  expect(test.names).toEqual([
    'addClean',
    'subscribed',
    'addClean',
    'addClean',
    'subscribed',
    'addClean',
    'addClean',
    'unsubscribed',
    'addClean'
  ])
  expect(test.reports[7][1]).toEqual({
    actionId: '3 10:a:uuid 0',
    channel: 'user/10'
  })
  expect(test.reports[8][1].action).toEqual({
    id: '3 10:a:uuid 0',
    type: 'logux/processed'
  })
  expect(test.app.subscribers).toEqual({
    posts: {
      '10:a:uuid': { filters: { '{}': filter } }
    }
  })
})

it('subscribes clients with multiple filters', async () => {
  let test = createReporter()
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  test.app.nodeIds.set('10:a:uuid', client)
  test.app.clientIds.set('10:a', client)

  let filter = (): boolean => false
  test.app.channel('posts', {
    access() {
      return true
    },
    async filter() {
      return filter
    }
  })

  await test.app.log.add(
    { channel: 'posts', type: 'logux/subscribe' },
    { id: '1 10:a:uuid 0' }
  )
  await test.app.log.add(
    { channel: 'posts', filter: { category: 'a' }, type: 'logux/subscribe' },
    { id: '1 10:a:uuid 0' }
  )
  await test.app.log.add(
    { channel: 'posts', filter: { category: 'b' }, type: 'logux/subscribe' },
    { id: '1 10:a:uuid 0' }
  )
  await setTimeout(1)
  expect(test.app.subscribers).toEqual({
    posts: {
      '10:a:uuid': {
        filters: {
          '{"category":"a"}': filter,
          '{"category":"b"}': filter,
          '{}': filter
        }
      }
    }
  })

  await test.app.log.add(
    { channel: 'posts', type: 'logux/unsubscribe' },
    { id: '2 10:a:uuid 0' }
  )
  await test.app.log.add(
    { channel: 'posts', filter: { category: 'b' }, type: 'logux/unsubscribe' },
    { id: '2 10:a:uuid 0' }
  )
  await setTimeout(1)
  expect(test.app.subscribers).toEqual({
    posts: {
      '10:a:uuid': {
        filters: { '{"category":"a"}': filter }
      }
    }
  })
})

it('cancels subscriptions on disconnect', async () => {
  let app = createServer()
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  app.nodeIds.set('10:uuid', client)
  app.clientIds.set('10:uuid', client)

  let cancels = 0
  app.on('subscriptionCancelled', () => {
    cancels += 1
  })

  app.channel('test', {
    access() {
      app.clientIds.delete('10:uuid')
      app.nodeIds.delete('10:uuid')
      return true
    },
    filter() {
      throw new Error('no calls')
    },
    load() {
      throw new Error('no calls')
    }
  })

  await app.log.add(
    { channel: 'test', type: 'logux/subscribe' },
    { id: '1 10:uuid 0' }
  )
  await setTimeout(10)

  expect(cancels).toEqual(1)
})

it('reports about errors during channel initialization', async () => {
  let test = createReporter()
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  test.app.nodeIds.set('10:uuid', client)
  test.app.clientIds.set('10:uuid', client)

  let err = new Error()
  test.app.channel(/^user\/(\d+)$/, {
    access: () => true,
    load() {
      throw err
    }
  })

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: '1 10:uuid 0' }
  )
  await setTimeout(1)

  expect(test.names).toEqual([
    'addClean',
    'subscribed',
    'error',
    'addClean',
    'unsubscribed'
  ])
  expect(test.reports[2][1]).toEqual({ actionId: '1 10:uuid 0', err })
  expect(test.reports[3][1].action).toEqual({
    action: { channel: 'user/10', type: 'logux/subscribe' },
    id: '1 10:uuid 0',
    reason: 'error',
    type: 'logux/undo'
  })
  expect(test.app.subscribers).toEqual({})
})

it('loads initial actions during subscription', async () => {
  let test = createReporter({ time: new TestTime() })
  let client: any = {
    node: { onAdd: () => false, remoteSubprotocol: 0 }
  }
  test.app.nodeIds.set('10:uuid', client)
  test.app.clientIds.set('10:uuid', client)

  test.app.on('preadd', (action, meta) => {
    meta.reasons.push('test')
  })

  let userLoaded = 0
  let initializating: (() => void) | undefined
  test.app.channel<{ id: string }>('user/:id', {
    access: () => true,
    load(ctx, action, meta) {
      expect(ctx.params.id).toEqual('10')
      expect(action.channel).toEqual('user/10')
      expect(meta.id).toEqual('1 10:uuid 0')
      expect(ctx.nodeId).toEqual('10:uuid')
      userLoaded += 1
      return new Promise(resolve => {
        initializating = resolve
      })
    }
  })

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: '1 10:uuid 0' }
  )
  await setTimeout(1)
  expect(userLoaded).toEqual(1)
  expect(test.app.subscribers).toEqual({
    'user/10': {
      '10:uuid': { filters: { '{}': true } }
    }
  })
  expect(test.app.log.actions()).toEqual([
    { channel: 'user/10', type: 'logux/subscribe' }
  ])
  if (typeof initializating === 'undefined') {
    throw new Error('callback is not set')
  }
  initializating()
  await setTimeout(1)

  expect(test.app.log.actions()).toEqual([
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: '1 10:uuid 0', type: 'logux/processed' }
  ])
})

it('calls unsubscribe() channel callback with logux/unsubscribe', async () => {
  let test = createReporter({})
  let client: any = {
    node: {
      onAdd: () => false,
      remoteHeaders: { preservedHeaders: true },
      remoteSubprotocol: 0
    }
  }
  let nodeId = '10:uuid'
  let clientId = '10:uuid'
  let userId = '10'
  test.app.nodeIds.set(nodeId, client)
  test.app.clientIds.set(clientId, client)
  test.app.on('preadd', (action, meta) => {
    meta.reasons.push('test')
  })
  let unsubscribeCallback = spy()
  test.app.channel<{ id: string }, { preservedData?: boolean }>('user/:id', {
    access(ctx) {
      ctx.data.preservedData = true
      return true
    },
    unsubscribe: unsubscribeCallback
  })

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: `1 ${nodeId}` }
  )
  expect(Object.keys(test.app.subscribers)).toHaveLength(1)

  await test.app.log.add(
    { channel: 'user/10', type: 'logux/unsubscribe' },
    { id: `2 ${nodeId}` }
  )
  expect(Object.keys(test.app.subscribers)).toHaveLength(0)

  expect(test.app.log.actions()).toEqual([
    { channel: 'user/10', type: 'logux/subscribe' },
    { id: `1 ${nodeId}`, type: 'logux/processed' },
    { channel: 'user/10', type: 'logux/unsubscribe' },
    { id: `2 ${nodeId}`, type: 'logux/processed' }
  ])
  expect(unsubscribeCallback.calls).toEqual([
    [
      expect.objectContaining({
        clientId,
        data: { preservedData: true },
        headers: { preservedHeaders: true },
        nodeId,
        params: { id: '10' },
        subprotocol: 0,
        userId
      }),
      expect.objectContaining({
        channel: 'user/10',
        type: 'logux/unsubscribe'
      }),
      expect.objectContaining({
        status: 'processed'
      })
    ]
  ])
})

it('does not need type definition for own actions', async () => {
  let test = createReporter()
  await test.app.log.add({ type: 'unknown' }, { users: ['10'] })
  expect(test.names).toEqual(['addClean'])
  expect(test.reports[0][1].action.type).toEqual('unknown')
  expect(test.reports[0][1].meta.status).toEqual('processed')
})

it('checks callbacks in unknown type handler', () => {
  let app = createServer()

  expect(() => {
    // @ts-expect-error
    app.otherType({ process: () => {} })
  }).toThrow('Unknown type must have access callback')

  app.otherType({ access: () => true })
  expect(() => {
    app.otherType({ access: () => true })
  }).toThrow('Callbacks for unknown types are already defined')
})

it('reports about useless actions', async () => {
  let test = createReporter()
  test.app.type('known', {
    access: () => true,
    process: () => {}
  })
  test.app.channel('a', { access: () => true })
  test.app.on('preadd', (action, meta) => {
    meta.reasons.push('test')
  })
  await test.app.log.add({ type: 'unknown' }, { status: 'processed' })
  await test.app.log.add({ type: 'known' })
  await test.app.log.add({ channel: 'a', type: 'logux/subscribe' })
  await test.app.log.add({ type: 'known' }, { channels: ['a'] })
  await test.app.log.add({ type: 'known' }, { users: ['10'] })
  await test.app.log.add({ type: 'known' }, { clients: ['10:client'] })
  await test.app.log.add({ type: 'known' }, { nodes: ['10:client:uuid'] })
  expect(test.names).toEqual([
    'add',
    'useless',
    'add',
    'add',
    'add',
    'add',
    'add',
    'add'
  ])
})

it('has shortcuts for resend arrays', async () => {
  let test = createReporter()
  test.app.type('A', {
    access: () => true,
    process: () => {}
  })
  test.app.on('preadd', (action, meta) => {
    meta.reasons.push('test')
  })
  await test.app.log.add(
    { type: 'A' },
    { channel: 'a', client: '1:1', node: '1:1:1', user: '1' }
  )
  expect(test.app.log.entries()).toEqual([
    [
      { type: 'A' },
      {
        added: 1,
        channels: ['a'],
        clients: ['1:1'],
        id: '1 server:uuid 0',
        nodes: ['1:1:1'],
        reasons: ['test'],
        server: 'server:uuid',
        status: 'waiting',
        subprotocol: 0,
        time: 1,
        users: ['1']
      }
    ]
  ])
  await setTimeout(10)
  expect(test.app.log.entries()).toEqual([
    [
      { type: 'A' },
      {
        added: 1,
        channels: ['a'],
        clients: ['1:1'],
        id: '1 server:uuid 0',
        nodes: ['1:1:1'],
        reasons: ['test'],
        server: 'server:uuid',
        status: 'processed',
        subprotocol: 0,
        time: 1,
        users: ['1']
      }
    ]
  ])
})

it('tracks action processing on add', async () => {
  let error = new Error('test')
  let app = createServer()
  app.type('FOO', {
    access: () => false,
    resend: () => ({ channels: ['foo'] })
  })
  app.type('ERROR', {
    access: () => false,
    process() {
      throw error
    }
  })

  let meta = await app.process({ type: 'FOO' }, { a: 1 })
  expect(meta.a).toEqual(1)
  expect(meta.channels).toEqual(['foo'])

  let err
  try {
    await app.process({ type: 'ERROR' })
  } catch (e) {
    err = e
  }
  expect(err).toBe(error)
})

it('has shortcut API for action creators', async () => {
  type ActionA = { aValue: string; type: 'A' }
  let createA = defineAction<ActionA>('A')

  let processed: string[] = []
  let app = createServer()
  app.type(createA, {
    access: () => true,
    process(ctx, action) {
      processed.push(action.aValue)
    }
  })

  await app.process(createA({ aValue: 'test' }))
  expect(processed).toEqual(['test'])
})

it('has alias to root from file URL', () => {
  let app = new BaseServer({
    fileUrl: import.meta.url,
    minSubprotocol: 1,
    subprotocol: 1
  })
  expect(app.options.root).toEqual(import.meta.dirname)
})

it('has custom logger', () => {
  let app = new BaseServer({
    minSubprotocol: 1,
    root: import.meta.dirname,
    subprotocol: 1
  })
  spyOn(console, 'warn', () => {})
  app.logger.warn({ test: 1 }, 'test')
  expect(calls(console.warn)).toEqual([[{ test: 1 }, 'test']])
})

it('subscribes clients manually', async () => {
  let app = new BaseServer({
    minSubprotocol: 1,
    root: import.meta.dirname,
    subprotocol: 1
  })
  let actions: Action[] = []
  app.log.on('add', (action, meta) => {
    expect(meta.nodes).toEqual(['test:1:1'])
    actions.push(action)
  })

  app.subscribe('test:1:1', 'users/10')
  await setTimeout(10)
  expect(app.subscribers).toEqual({
    'users/10': {
      'test:1:1': { filters: { '{}': true } }
    }
  })
  expect(actions).toEqual([{ channel: 'users/10', type: 'logux/subscribed' }])

  app.subscribe('test:1:1', 'users/10')
  await setTimeout(10)
  expect(actions).toEqual([{ channel: 'users/10', type: 'logux/subscribed' }])
})

it('processes action with accessAndProcess callback', async () => {
  let test = createReporter()
  let accessAndProcess = spy()
  test.app.type('A', {
    accessAndProcess
  })
  await test.app.process({ type: 'A' })
  expect(accessAndProcess.callCount).toEqual(1)
})


================================================
FILE: context/index.d.ts
================================================
import type { AnyAction } from '@logux/core'

import type { ServerMeta } from '../base-server/index.js'
import type { ServerClient } from '../server-client/index.js'
import type { Server } from '../server/index.js'

export class ConnectContext<Headers extends object = unknown> {
  /**
   * Unique persistence client ID.
   *
   * ```js
   * server.clientIds.get(node.clientId)
   * ```
   */
  clientId: string

  /**
   * Client’s headers.
   *
   * ```js
   * ctx.sendBack({
   *   type: 'error',
   *   message: I18n[ctx.headers.locale || 'en'].error
   * })
   * ```
   */
  headers: Headers

  /**
   * Unique node ID.
   *
   * ```js
   * server.nodeIds.get(node.nodeId)
   * ```
   */
  nodeId: string

  /**
   * Logux server
   */
  server: Server

  /**
   * Action creator application subprotocol version.
   */
  subprotocol: number

  /**
   * User ID taken node ID.
   *
   * ```js
   * async access (ctx, action, meta) {
   *   const user = await db.getUser(ctx.userId)
   *   return user.admin
   * }
   * ```
   */
  userId: string

  constructor(server: Server, client: ServerClient)

  /**
   * Send action back to the client.
   *
   * ```js
   * ctx.sendBack({ type: 'login/success', token })
   * ```
   *
   * Action will not be processed by server’s callbacks from `Server#type`.
   *
   * @param action The action.
   * @param meta Action’s meta.
   * @returns Promise until action was added to the server log.
   */
  sendBack(action: AnyAction, meta?: Partial<ServerMeta>): Promise<void>
}

/**
 * Action context.
 * ```
 */
export class Context<
  Data extends object = unknown,
  Headers extends object = unknown
> extends ConnectContext<Headers> {
  /**
   * Open structure to save some data between different steps of processing.
   *
   * ```js
   * server.type('RENAME', {
   *   access (ctx, action, meta) {
   *     ctx.data.user = findUser(ctx.userId)
   *     return ctx.data.user.hasAccess(action.projectId)
   *   }
   *   process (ctx, action, meta) {
   *     return ctx.data.user.rename(action.projectId, action.name)
   *   }
   * })
   * ```
   */
  data: Data

  /**
   * Was action created by Logux server.
   *
   * ```js
   * access: (ctx, action, meta) => ctx.isServer
   * ```
   */
  isServer: boolean

  constructor(server: Server, meta: ServerMeta)
}

/**
 * Subscription context.
 *
 * ```js
 * server.channel('user/:id', {
 *   access (ctx, action, meta) {
 *     return ctx.params.id === ctx.userId
 *   }
 * })
 * ```
 */
export class ChannelContext<
  Data extends object,
  ChannelParams extends object | string[],
  Headers extends object
> extends Context<Data, Headers> {
  /**
   * Parsed variable parts of channel pattern.
   *
   * ```js
   * server.channel('user/:id', {
   *   access (ctx, action, meta) {
   *     action.channel //=> user/10
   *     ctx.params //=> { id: '10' }
   *   }
   * })
   * server.channel(/post\/(\d+)/, {
   *   access (ctx, action, meta) {
   *     action.channel //=> post/10
   *     ctx.params //=> ['post/10', '10']
   *   }
   * })
   * ```
   */
  params: ChannelParams
}


================================================
FILE: context/index.js
================================================
import { parseId } from '@logux/core'

export class Context {
  constructor(server, meta) {
    this.server = server
    this.data = {}

    let client
    if (meta.node) {
      client = meta
      this.nodeId = client.nodeId
      this.userId = client.userId
      this.clientId = client.clientId
      this.subprotocol = client.node.remoteSubprotocol
    } else {
      let parsed = parseId(meta.id)
      this.nodeId = parsed.nodeId
      this.userId = parsed.userId
      this.clientId = parsed.clientId
      this.isServer = this.userId === 'server'
      client = server.clientIds.get(this.clientId)
      if (meta.subprotocol) {
        this.subprotocol = meta.subprotocol
      } else if (client) {
        this.subprotocol = client.node.remoteSubprotocol
      }
    }

    if (client) {
      this.headers = client.node.remoteHeaders
    } else {
      this.headers = {}
    }
  }

  sendBack(action, meta = {}) {
    return this.server.log.add(action, {
      clients: [this.clientId],
      status: 'processed',
      ...meta
    })
  }
}


================================================
FILE: context/index.test.ts
================================================
import type { Action } from '@logux/core'
import { beforeEach, expect, it } from 'vitest'

import { Context, type ServerMeta } from '../index.js'

let added: [Action, ServerMeta][] = []

const FAKE_SERVER: any = {
  clientIds: new Map([
    [
      '20:client',
      { node: { remoteHeaders: { locale: 'fr' }, remoteSubprotocol: 2 } }
    ]
  ]),

  log: {
    add(action: Action, meta: ServerMeta) {
      added.push([action, meta])
      return Promise.resolve()
    }
  }
}

beforeEach(() => {
  added = []
})

function createContext(
  meta: Partial<ServerMeta> = { id: '1 10:client:uuid 0', subprotocol: 1 }
): Context {
  return new Context(FAKE_SERVER, meta as ServerMeta)
}

it('has open data', () => {
  let ctx = createContext()
  expect(ctx.data).toEqual({})
})

it('parses meta', () => {
  let ctx = createContext()
  expect(ctx.nodeId).toEqual('10:client:uuid')
  expect(ctx.clientId).toEqual('10:client')
  expect(ctx.userId).toEqual('10')
  expect(ctx.subprotocol).toEqual(1)
})

it('detects servers', () => {
  let user = createContext({ id: '1 10:uuid 0' })
  expect(user.isServer).toBe(false)
  let server = createContext({ id: '1 server:uuid 0' })
  expect(server.isServer).toBe(true)
})

it('takes subprotocol from client', () => {
  let ctx = createContext({ id: '1 20:client:uuid 0' })
  expect(ctx.subprotocol).toEqual(2)
})

it('works on missed subprotocol', () => {
  let ctx = createContext({ id: '1 10:client:uuid 0' })
  expect(ctx.subprotocol).toBeUndefined()
})

it('takes headers from client', () => {
  let ctx = createContext({ id: '1 20:client:uuid 0' })
  expect(ctx.headers).toEqual({ locale: 'fr' })
})

it('works on missed headers', () => {
  let ctx = createContext({ id: '1 10:client:uuid 0' })
  expect(ctx.headers).toEqual({})
})

it('sends action back', () => {
  let ctx = createContext()
  expect(ctx.sendBack({ type: 'A' }) instanceof Promise).toBe(true)
  ctx.sendBack({ type: 'B' }, { clients: [], reasons: ['1'] })
  expect(added).toEqual([
    [{ type: 'A' }, { clients: ['10:client'], status: 'processed' }],
    [{ type: 'B' }, { clients: [], reasons: ['1'], status: 'processed' }]
  ])
})


================================================
FILE: create-http-server/index.js
================================================
import { promises as fs } from 'node:fs'
import http from 'node:http'
import https from 'node:https'
import { isAbsolute, join } from 'node:path'

const PEM_PREAMBLE = '-----BEGIN'

function isPem(content) {
  if (typeof content === 'object' && content.pem) {
    return true
  } else {
    return content.toString().trim().startsWith(PEM_PREAMBLE)
  }
}

function readFrom(root, file) {
  file = file.toString()
  if (!isAbsolute(file)) file = join(root, file)
  return fs.readFile(file)
}

export async function createHttpServer(opts) {
  let server
  if (opts.server) {
    server = opts.server
  } else {
    let key = opts.key
    let cert = opts.cert
    if (key && !isPem(key)) key = await readFrom(opts.root, key)
    if (cert && !isPem(cert)) cert = await readFrom(opts.root, cert)

    if (key && key.pem) {
      server = https.createServer({ cert, key: key.pem })
    } else if (key) {
      server = https.createServer({ cert, key })
    } else {
      server = http.createServer()
    }
  }

  return server
}


================================================
FILE: create-reporter/__snapshots__/index.test.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`allows custom loggers 1`] = `"{"details":{"connectionId":"670","ipAddress":"10.110.6.56"},"msg":"Client was connected"}"`;

exports[`reports EACCES error 1`] = `
"{"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\`"}
FLUSH"
`;

exports[`reports EACCES error 2`] = `
" FATAL  You are not allowed to run server on port 80 at 1970-01-01 00:00:00
        Non-privileged users can't start a listening socket on ports below 1024.
        Try to change user or take another port.
        
        $ su - <username>
        $ npm start -p 80

"
`;

exports[`reports actions with excludeClients metadata 1`] = `
"{"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"}
"
`;

exports[`reports actions with excludeClients metadata 2`] = `
" INFO   Action was added at 1970-01-01 00:00:00
        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

"
`;

exports[`reports actions with metadata containing 'clients' array 1`] = `
"{"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"}
"
`;

exports[`reports actions with metadata containing 'clients' array 2`] = `
" INFO   Action was added at 1970-01-01 00:00:00
        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

"
`;

exports[`reports add 1`] = `
"{"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"}
"
`;

exports[`reports add 2`] = `
" INFO   Action was added at 1970-01-01 00:00:00
        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

"
`;

exports[`reports add and clean 1`] = `
"{"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"}
"
`;

exports[`reports add and clean 2`] = `
" INFO   Action was added and cleaned at 1970-01-01 00:00:00
        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

"
`;

exports[`reports authenticated 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"connectionId":"670","nodeId":"admin:100:uImkcF4z","subprotocol":1,"msg":"User was authenticated"}
"
`;

exports[`reports authenticated 2`] = `
" INFO   User was authenticated at 1970-01-01 00:00:00
        Connection ID: 670
        Node ID:       admin:100
        Subprotocol:   1

"
`;

exports[`reports authenticated without user ID 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"connectionId":"670","nodeId":"uImkcF4z","subprotocol":1,"msg":"User was authenticated"}
"
`;

exports[`reports authenticated without user ID 2`] = `
" INFO   User was authenticated at 1970-01-01 00:00:00
        Connection ID: 670
        Node ID:       uImkcF4z
        Subprotocol:   1

"
`;

exports[`reports clean 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1487805099387 100:uImkcF4z 0","msg":"Action was cleaned"}
"
`;

exports[`reports clean 2`] = `
" INFO   Action was cleaned at 1970-01-01 00:00:00
        Action ID: 1487805099387 100:uImkcF4z 0

"
`;

exports[`reports connect 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"connectionId":"670","ipAddress":"10.110.6.56","msg":"Client was connected"}
"
`;

exports[`reports connect 2`] = `
" INFO   Client was connected at 1970-01-01 00:00:00
        Connection ID: 670
        IP address:    10.110.6.56

"
`;

exports[`reports denied 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1487805099387 100:uImkcF4z 0","msg":"Action was denied"}
"
`;

exports[`reports denied 2`] = `
" WARN   Action was denied at 1970-01-01 00:00:00
        Action ID: 1487805099387 100:uImkcF4z 0

"
`;

exports[`reports destroy 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"msg":"Shutting down Logux server"}
"
`;

exports[`reports destroy 2`] = `
" INFO   Shutting down Logux server at 1970-01-01 00:00:00

"
`;

exports[`reports disconnect 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"nodeId":"100:uImkcF4z","msg":"Client was disconnected"}
"
`;

exports[`reports disconnect 2`] = `
" INFO   Client was disconnected at 1970-01-01 00:00:00
        Node ID: 100:uImkcF4z

"
`;

exports[`reports disconnect from unauthenticated user 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"connectionId":"670","msg":"Client was disconnected"}
"
`;

exports[`reports disconnect from unauthenticated user 2`] = `
" INFO   Client was disconnected at 1970-01-01 00:00:00
        Connection ID: 670

"
`;

exports[`reports error 1`] = `
"{"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"}
FLUSH"
`;

exports[`reports error 2`] = `
" FATAL  Some mistake at 1970-01-01 00:00:00
        at Object.<anonymous> (/dev/app/index.js:28:13)
        at Module._compile (module.js:573:32)
        at at runTest (/dev/app/node_modules/jest/index.js:50:10)
        at process._tickCallback (internal/process/next_tick.js:103:7)

"
`;

exports[`reports error from action 1`] = `
"{"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"}
"
`;

exports[`reports error from action 2`] = `
" ERROR  Some mistake at 1970-01-01 00:00:00
        at Object.<anonymous> (/dev/app/index.js:28:13)
        at Module._compile (module.js:573:32)
        at at runTest (/dev/app/node_modules/jest/index.js:50:10)
        at process._tickCallback (internal/process/next_tick.js:103:7)
        Action ID: 1487805099387 100:uImkcF4z 0

"
`;

exports[`reports error from client 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"connectionId":"670","msg":"Client error: A timeout was reached (5000 ms)"}
"
`;

exports[`reports error from client 2`] = `
" WARN   Client error: A timeout was reached (5000 ms) at 1970-01-01 00:00:00
        Connection ID: 670

"
`;

exports[`reports error from node 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"nodeId":"100:uImkcF4z","msg":"Sync error: A timeout was reached (5000 ms)"}
"
`;

exports[`reports error from node 2`] = `
" WARN   Sync error: A timeout was reached (5000 ms) at 1970-01-01 00:00:00
        Node ID: 100:uImkcF4z

"
`;

exports[`reports error with token 1`] = `
"{"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]\\"}"}
"
`;

exports[`reports error with token 2`] = `
" ERROR  {"Authorization":"[SECRET]"} at 1970-01-01 00:00:00
        at Object.<anonymous> (/dev/app/index.js:28:13)
        at Module._compile (module.js:573:32)
        at at runTest (/dev/app/node_modules/jest/index.js:50:10)
        at process._tickCallback (internal/process/next_tick.js:103:7)
        Action ID: 1487805099387 100:uImkcF4z 0

"
`;

exports[`reports listen 1`] = `
"{"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"}
"
`;

exports[`reports listen 2`] = `
" INFO   Logux server is listening at 1970-01-01 00:00:00
        PID:             21384
        Environment:     development
        Logux server:    0.0.0
        Min subprotocol: 0
        Node ID:         server:FnXaqDxY
        Subprotocol:     0
        Health check:    http://127.0.0.1:31337/health
        Listen:          ws://127.0.0.1:31337/
        Server was started in non-secure development mode
        Press Ctrl-C to shutdown server

"
`;

exports[`reports listen for custom domain 1`] = `
"{"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"}
"
`;

exports[`reports listen for custom domain 2`] = `
" INFO   Logux server is listening at 1970-01-01 00:00:00
        PID:             21384
        Environment:     development
        Logux server:    0.0.0
        Min subprotocol: 0
        Node ID:         server:FnXaqDxY
        Subprotocol:     0
        Prometheus:      http://127.0.0.1:31338/prometheus
        Listen:          Custom HTTP server
        Server was started in non-secure development mode
        Press Ctrl-C to shutdown server

"
`;

exports[`reports listen for production 1`] = `
"{"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"}
"
`;

exports[`reports listen for production 2`] = `
" INFO   Logux server is listening at 1970-01-01 00:00:00
        PID:             21384
        Environment:     production
        Logux server:    0.0.0
        Min subprotocol: 0
        Node ID:         server:FnXaqDxY
        Subprotocol:     0
        Health check:    https://127.0.0.1:31337/health
        Redis:           //localhost
        Listen:          wss://127.0.0.1:31337/

"
`;

exports[`reports subscribed 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1487805099387 100:uImkcF4z 0","channel":"user/100","msg":"Client was subscribed"}
"
`;

exports[`reports subscribed 2`] = `
" INFO   Client was subscribed at 1970-01-01 00:00:00
        Action ID: 1487805099387 100:uImkcF4z 0
        Channel:   user/100

"
`;

exports[`reports unauthenticated 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"connectionId":"670","nodeId":"100:uImkcF4z","subprotocol":1,"msg":"Bad authentication"}
"
`;

exports[`reports unauthenticated 2`] = `
" WARN   Bad authentication at 1970-01-01 00:00:00
        Connection ID: 670
        Node ID:       100:uImkcF4z
        Subprotocol:   1

"
`;

exports[`reports unknownType 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1487805099387 100:vAApgNT9 0","type":"CHANGE_SER","msg":"Action with unknown type"}
"
`;

exports[`reports unknownType 2`] = `
" WARN   Action with unknown type at 1970-01-01 00:00:00
        Action ID: 1487805099387 100:vAApgNT9 0
        Type:      CHANGE_SER

"
`;

exports[`reports unknownType from server 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1650269021700 server:FnXaqDxY 0","type":"CHANGE_SER","msg":"Action with unknown type"}
"
`;

exports[`reports unknownType from server 2`] = `
" WARN   Action with unknown type at 1970-01-01 00:00:00
        Action ID: 1650269021700 server:FnXaqDxY 0
        Type:      CHANGE_SER

"
`;

exports[`reports unsubscribed 1`] = `
"{"level":30,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1650271940900 100:uImkcF4z 0","channel":"user/100","msg":"Client was unsubscribed"}
"
`;

exports[`reports unsubscribed 2`] = `
" INFO   Client was unsubscribed at 1970-01-01 00:00:00
        Action ID: 1650271940900 100:uImkcF4z 0
        Channel:   user/100

"
`;

exports[`reports useless actions 1`] = `
"{"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"}
"
`;

exports[`reports useless actions 2`] = `
" WARN   Useless action at 1970-01-01 00:00:00
        Action: 
          id:   100
          name: "John"
          type: "ADD_USER"
        Meta:   
          id:          1487805099387 100:uImkcF4z 0
          reasons:     []
          server:      server:H1f8LAyzl
          subprotocol: 1
          time:        1487805099387

"
`;

exports[`reports wrongChannel 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1650269045800 100:IsvVzqWx 0","channel":"ser/100","msg":"Wrong channel name"}
"
`;

exports[`reports wrongChannel 2`] = `
" WARN   Wrong channel name at 1970-01-01 00:00:00
        Action ID: 1650269045800 100:IsvVzqWx 0
        Channel:   ser/100

"
`;

exports[`reports wrongChannel without name 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"actionId":"1650269056600 100:uImkcF4z 0","msg":"Wrong channel name"}
"
`;

exports[`reports wrongChannel without name 2`] = `
" WARN   Wrong channel name at 1970-01-01 00:00:00
        Action ID: 1650269056600 100:uImkcF4z 0
        Channel:   undefined

"
`;

exports[`reports zombie 1`] = `
"{"level":40,"time":"1970-01-01T00:00:00.000Z","pid":21384,"nodeId":"100:uImkcF4z","msg":"Zombie client was disconnected"}
"
`;

exports[`reports zombie 2`] = `
" WARN   Zombie client was disconnected at 1970-01-01 00:00:00
        Node ID: 100:uImkcF4z

"
`;

exports[`stacktrace > reports EADDRINUSE error 1`] = `
"{"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"}
FLUSH"
`;

exports[`stacktrace > reports EADDRINUSE error 2`] = `
" FATAL  Port 31337 already in use at 1970-01-01 00:00:00
        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.
        
        $ su - root
        # netstat -nlp | grep 31337
        Proto   Local Address   State    PID/Program name
        tcp     0.0.0.0:31337   LISTEN   777/node
        # sudo kill -9 777

"
`;

exports[`stacktrace > reports Logux error 1`] = `
"{"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"}
FLUSH"
`;

exports[`stacktrace > reports Logux error 2`] = `
" FATAL  Unknown option suprotocol in server constructor at 1970-01-01 00:00:00
        Maybe there is a mistake in option name or this version of Logux Server
        doesn’t support this option

"
`;

exports[`stacktrace > reports sync error 1`] = `
"{"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)"}
"
`;

exports[`stacktrace > reports sync error 2`] = `
" ERROR  Logux received unknown-message error (Unknown message bad type) at 1970-01-01 00:00:00
        Connection ID: 670

"
`;


================================================
FILE: create-reporter/index.js
================================================
import os from 'node:os'
import { sep } from 'node:path'

import humanFormatter from '../human-formatter/index.js'

const ERROR_CODES = {
  EACCES: (e, environment) => {
    let wayToFix = {
      development: 'In dev mode it can be done with sudo:\n$ sudo npm start',
      production: `$ su - \`<username>\`\n$ npm start -p ${e.port}`
    }

    return {
      msg: `You are not allowed to run server on port \`${e.port}\``,
      note:
        "Non-privileged users can't start a listening socket on ports " +
        'below 1024. Try to change user or take another port.\n\n' +
        (wayToFix[environment] || wayToFix.production)
    }
  },
  EADDRINUSE: e => {
    let port = String(e.port)
    let wayToFix = {
      darwin: `$ sudo lsof -i ${e.port}\n$ sudo kill -9 \`<processid>\``,
      linux:
        '$ su - root\n' +
        `# netstat -nlp | grep ${e.port}\n` +
        'Proto   Local Address   State    PID/Program name\n' +
        `tcp     0.0.0.0:${port.padEnd(8)}LISTEN   \`777\`/node\n` +
        '# sudo kill -9 `777`',
      win32:
        'Run `cmd.exe` as an administrator\n' +
        'C:\\> netstat -a -b -n -o\n' +
        'C:\\> taskkill /F /PID `<processid>`'
    }

    return {
      msg: `Port \`${e.port}\` already in use`,
      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' +
        (wayToFix[os.platform()] || '')
    }
  }
}

const REPORTERS = {
  add: () => ({ msg: 'Action was added' }),

  addClean: () => ({ msg: 'Action was added and cleaned' }),

  authenticated: () => ({ msg: 'User was authenticated' }),

  clean: () => ({ msg: 'Action was cleaned' }),

  clientError: record => {
    let result = {
      details: {},
      level: 'warn'
    }
    if (record.err.received) {
      result.msg = `Client error: ${record.err.description}`
    } else {
      result.msg = `Sync error: ${record.err.description}`
    }
    for (let i in record) {
      if (i !== 'err') {
        result.details[i] = record[i]
      }
    }
    return result
  },

  connect: () => ({ msg: 'Client was connected' }),

  denied: () => ({ level: 'warn', msg: 'Action was denied' }),

  destroy: () => ({ msg: 'Shutting down Logux server' }),

  disconnect: () => ({ msg: 'Client was disconnected' }),

  error: record => {
    let result = {
      details: {
        err: {
          message: record.err.message,
          name: record.err.name,
          stack: record.err.stack
        }
      },
      level: record.fatal ? 'fatal' : 'error',
      msg: record.err.message
    }

    let helper = ERROR_CODES[record.err.code]
    if (helper) {
      let help = helper(record.err, record.environment)
      result.msg = help.msg
      result.details.note = help.note
      delete result.details.err.stack
    } else if (record.err.logux) {
      result.details.note = record.err.note
      delete result.details.err
    }

    if (record.err.name === 'LoguxError') {
      delete result.details.err.stack
    }

    for (let i in record) {
      if (i !== 'err' && i !== 'fatal') {
        result.details[i] = record[i]
      }
    }

    return result
  },

  listen: r => {
    let details = {
      environment: r.environment,
      loguxServer: r.loguxServer,
      minSubprotocol: r.minSubprotocol,
      nodeId: r.nodeId,
      subprotocol: r.subprotocol
    }

    if (r.environment === 'development') {
      details.note = [
        'Server was started in non-secure development mode',
        'Press Ctrl-C to shutdown server'
      ]
    }

    if (r.server) {
      details.server = r.server
    } else {
      let wsProtocol = r.cert ? 'wss://' : 'ws://'
      let httpProtocol = r.cert ? 'https://' : 'http://'
      details.listen = `${wsProtocol}${r.host}:${r.port}/`
      details.healthCheck = `${httpProtocol}${r.host}:${r.port}/health`
    }

    if (r.redis) {
      details.redis = r.redis
    }

    for (let i in r.notes) details[i] = r.notes[i]

    return { details, msg: 'Logux server is listening' }
  },

  subscribed: () => ({ msg: 'Client was subscribed' }),

  unauthenticated: () => ({ level: 'warn', msg: 'Bad authentication' }),

  unknownType: record => ({
    level: /^ server(:| )/.test(record.actionId) ? 'error' : 'warn',
    msg: 'Action with unknown type'
  }),

  unsubscribed: () => ({ msg: 'Client was unsubscribed' }),

  useless: () => ({ level: 'warn', msg: 'Useless action' }),

  wrongChannel: () => ({
    level: 'warn',
    msg: 'Wrong channel name'
  }),

  zombie: () => ({ level: 'warn', msg: 'Zombie client was disconnected' })
}

function cleanFromKeys(obj, regexp, seen) {
  let result = {}
  for (let key in obj) {
    let v = obj[key]
    if (typeof v === 'string') {
      result[key] = v.replace(regexp, '[SECRET]')
    } else if (typeof v === 'object' && !Array.isArray(v) && v !== null) {
      if (seen.includes(v)) {
        throw new Error('Circular reference in action')
      }
      seen.push(v)
      result[key] = cleanFromKeys(v, regexp, seen)
      seen.pop()
    } else {
      result[key] = v
    }
  }
  return result
}

function createRecord(level, details, msg) {
  /* c8 ignore next 4 */
  if (typeof details === 'string') {
    msg = details
    details = {}
  }
  return {
    level,
    time: new Date().toISOString(),
    pid: process.pid,
    ...details,
    msg
  }
}

export function createReporter(options) {
  let cleanFromLog = options.cleanFromLog || /Bearer [^\s"]+/g
  function reporter(type, details) {
    let report = REPORTERS[type](details)
    let level = report.level || 'info'
    let seen = []
    reporter.logger[level](
      cleanFromKeys(report.details || details || {}, cleanFromLog, seen),
      report.msg.replace(cleanFromLog, '[SECRET]')
    )
  }

  if (typeof options.logger !== 'string' && 'info' in options.logger) {
    reporter.logger = options.logger
  } else {
    let format
    if (options.logger === 'human' || options.logger.type === 'human') {
      let basepath = options.root || process.cwd()
      if (basepath.slice(-1) !== sep) basepath += sep
      format = humanFormatter({ basepath })
    } else {
      format = record => JSON.stringify(record) + '\n'
    }
    let stream = options.logger?.stream ?? process.stderr

    reporter.logger = {
      /* c8 ignore next 3 */
      debug(details, msg) {
        stream.write(format(createRecord(20, details, msg)))
      },
      info(details, msg) {
        stream.write(format(createRecord(30, details, msg)))
      },
      warn(details, msg) {
        stream.write(format(createRecord(40, details, msg)))
      },
      error(details, msg) {
        stream.write(format(createRecord(50, details, msg)))
      },
      fatal(details, msg) {
        if (stream.flushSync) {
          stream.flushSync(format(createRecord(60, details, msg)))
        } else {
          stream.write(format(createRecord(60, details, msg)))
        }
      }
    }
  }
  return reporter
}


================================================
FILE: create-reporter/index.test.ts
================================================
import '../test/force-colors.js'

import { LoguxError } from '@logux/core'
import { describe, expect, it } from 'vitest'

import { createReporter } from './index.js'

class MemoryStream {
  flushSync: ((chunk: string) => void) | undefined

  string: string

  constructor(flushSync: boolean) {
    this.string = ''
    if (flushSync) {
      this.flushSync = chunk => {
        this.string += chunk + 'FLUSH'
      }
    }
  }

  write(chunk: string): void {
    this.string += chunk
  }
}

function clean(str: string): string {
  let cleaned = str
    .replace(/\r\v/g, '\n')
    .replace(/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/g, '1970-01-01 00:00:00')
    .replace(/"time":"[^"]+"/g, '"time":"1970-01-01T00:00:00.000Z"')
    .replace(/"hostname":"[^"]+"/g, '"hostname":"localhost"')
    .replace(/"pid":\d+/g, '"pid":21384')
    .replace(/PID:(\s+.*m)\d+(.*m)/, 'PID:$121384$2')
  return cleaned
}

function check(type: string, details?: object): void {
  let json = new MemoryStream(true)
  let jsonReporter = createReporter({
    logger: { stream: json, type: 'json' }
  })
  jsonReporter(type, details)
  expect(clean(json.string)).toMatchSnapshot()

  let human = new MemoryStream(false)
  let humanReporter = createReporter({
    logger: { stream: human, type: 'human' }
  })
  humanReporter(type, details)
  expect(clean(human.string)).toMatchSnapshot()
}

function createError(name: string, message: string): Error {
  let err = new Error(message)
  err.name = name
  err.stack =
    `${name}: ${message}\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)'
  return err
}

it('reports listen', () => {
  check('listen', {
    cert: false,
    environment: 'development',
    host: '127.0.0.1',
    loguxServer: '0.0.0',
    minSubprotocol: 0,
    nodeId: 'server:FnXaqDxY',
    notes: {},
    port: 31337,
    redis: undefined,
    server: false,
    subprotocol: 0
  })
})

it('reports listen for production', () => {
  check('listen', {
    cert: true,
    environment: 'production',
    host: '127.0.0.1',
    loguxServer: '0.0.0',
    minSubprotocol: 0,
    nodeId: 'server:FnXaqDxY',
    notes: {},
    port: 31337,
    redis: '//localhost',
    server: false,
    subprotocol: 0
  })
})

it('reports listen for custom domain', () => {
  check('listen', {
    environment: 'development',
    loguxServer: '0.0.0',
    minSubprotocol: 0,
    nodeId: 'server:FnXaqDxY',
    notes: {
      prometheus: 'http://127.0.0.1:31338/prometheus'
    },
    server: true,
    subprotocol: 0
  })
})

it('reports connect', () => {
  check('connect', { connectionId: '670', ipAddress: '10.110.6.56' })
})

it('reports authenticated', () => {
  check('authenticated', {
    connectionId: '670',
    nodeId: 'admin:100:uImkcF4z',
    subprotocol: 1
  })
})

it('reports authenticated without user ID', () => {
  check('authenticated', {
    connectionId: '670',
    nodeId: 'uImkcF4z',
    subprotocol: 1
  })
})

it('reports unauthenticated', () => {
  check('unauthenticated', {
    connectionId: '670',
    nodeId: '100:uImkcF4z',
    subprotocol: 1
  })
})

it('reports add', () => {
  check('add', {
    action: {
      data: {
        array: [1, [2], { a: '1', b: { c: 2 }, d: [], e: null }, null],
      
Download .txt
gitextract_s1og6kor/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── api.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── add-http-pages/
│   ├── hello.html
│   ├── index.js
│   └── index.test.ts
├── add-sync-map/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── allowed-meta/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── base-server/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── context/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── create-http-server/
│   └── index.js
├── create-reporter/
│   ├── __snapshots__/
│   │   └── index.test.ts.snap
│   ├── index.js
│   └── index.test.ts
├── filter-meta/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── filtered-node/
│   ├── index.js
│   └── index.test.ts
├── human-formatter/
│   ├── index.js
│   └── utils.js
├── index.d.ts
├── index.js
├── options-loader/
│   ├── __snapshots__/
│   │   └── index.test.js.snap
│   ├── index.js
│   ├── index.test.js
│   └── test.env
├── oxfmt.config.ts
├── oxlint.config.ts
├── package.json
├── request/
│   ├── index.d.ts
│   └── index.js
├── server/
│   ├── __snapshots__/
│   │   └── index.test.ts.snap
│   ├── errors.ts
│   ├── index.d.ts
│   ├── index.js
│   ├── index.test.ts
│   └── types.ts
├── server-client/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── test/
│   ├── fixtures/
│   │   ├── cert.pem
│   │   └── key.pem
│   ├── force-colors.js
│   └── servers/
│       ├── autoload-error-modules.js
│       ├── autoload-modules.js
│       ├── destroy.js
│       ├── eacces.js
│       ├── eaddrinuse.js
│       ├── error-modules/
│       │   └── wrond-export/
│       │       └── index.js
│       ├── json.js
│       ├── logger.js
│       ├── missed.js
│       ├── modules/
│       │   ├── child/
│       │   │   ├── index.foo.js
│       │   │   ├── index.js
│       │   │   └── lib/
│       │   │       └── lib.js
│       │   ├── root.js
│       │   └── root.test.js
│       ├── options.js
│       ├── root.js
│       ├── throw.js
│       ├── unbind.js
│       ├── uncatch.js
│       └── unknown.js
├── test-client/
│   ├── index.d.ts
│   ├── index.js
│   └── index.test.ts
├── test-server/
│   ├── index.d.ts
│   └── index.js
├── tsconfig.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (415 symbols across 38 files)

FILE: add-http-pages/index.js
  function readHello (line 5) | async function readHello() {
  function addHttpPages (line 12) | function addHttpPages(server) {

FILE: add-http-pages/index.test.ts
  constant DEFAULT_OPTIONS (line 12) | const DEFAULT_OPTIONS = {
  function createServer (line 19) | function createServer(
  class RequestError (line 45) | class RequestError extends Error {
    method constructor (line 48) | constructor(statusCode: number | undefined, body: string) {
  type HttpResponse (line 55) | interface HttpResponse {
  function request (line 60) | function request(
  function requestError (line 93) | async function requestError(

FILE: add-sync-map/index.d.ts
  type WithTime (line 18) | type WithTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
  type WithoutTime (line 24) | type WithoutTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
  type SyncMapData (line 30) | type SyncMapData<Value extends SyncMapValues> = {
  type SyncMapActionFilter (line 59) | interface SyncMapActionFilter<Value extends SyncMapValues> {
  type SyncMapOperations (line 70) | interface SyncMapOperations<Value extends SyncMapValues> {
  type SyncMapFilterOperations (line 116) | interface SyncMapFilterOperations<Value extends SyncMapValues> {

FILE: add-sync-map/index.js
  constant WITH_TIME (line 1) | const WITH_TIME = Symbol('WITH_TIME')
  function ChangedAt (line 3) | function ChangedAt(value, time) {
  function NoConflictResolution (line 7) | function NoConflictResolution(value) {
  function addFinished (line 11) | async function addFinished(server, ctx, type, action, meta) {
  function resendFinished (line 18) | function resendFinished(server, plural, type, all = true) {
  function buildFilter (line 40) | function buildFilter(filter) {
  function sendMap (line 58) | async function sendMap(server, changedType, data, since) {
  function addSyncMap (line 90) | function addSyncMap(server, plural, operations) {
  function addSyncMapFilter (line 177) | function addSyncMapFilter(server, plural, operations) {

FILE: add-sync-map/index.test.ts
  type TaskValue (line 20) | type TaskValue = {
  type TaskRecord (line 25) | type TaskRecord = {
  type CommentValue (line 39) | type CommentValue = {
  function getTime (line 56) | function getTime(client: TestClient, creator: { type: string }): number[] {
  function getServer (line 63) | function getServer(): TestServer {
  method access (line 333) | access() {
  method load (line 336) | load(ctx, id, since) {
  method access (line 352) | access() {
  method initial (line 355) | initial() {
  method access (line 383) | access() {
  method change (line 386) | change(ctx, id) {
  method create (line 389) | create(ctx, id) {
  method delete (line 392) | delete(ctx, id) {
  method load (line 395) | load(ctx, id) {
  method access (line 400) | access() {
  method initial (line 403) | initial() {
  method access (line 434) | access() {
  method load (line 437) | load(ctx, id) {
  method access (line 464) | access() {
  method load (line 468) | load(ctx, id) {

FILE: allowed-meta/index.d.ts
  constant ALLOWED_META (line 17) | const ALLOWED_META: string[]

FILE: allowed-meta/index.js
  constant ALLOWED_META (line 1) | const ALLOWED_META = ['id', 'time', 'subprotocol']

FILE: base-server/index.d.ts
  type LogFn (line 31) | interface LogFn {
  type TypeOptions (line 35) | interface TypeOptions {
  type ChannelOptions (line 43) | interface ChannelOptions {
  type ConnectLoader (line 51) | interface ConnectLoader<Headers extends object = unknown> {
  type ServerNodeConstructor (line 66) | type ServerNodeConstructor = new (...args: unknown[]) => ServerNode
  type ServerMeta (line 68) | interface ServerMeta extends Meta {
  type BaseServerOptions (line 125) | interface BaseServerOptions {
  type AuthenticatorOptions (line 253) | interface AuthenticatorOptions<Headers extends object> {
  type SendBackActions (line 261) | type SendBackActions =
  type ServerAuthenticator (line 275) | interface ServerAuthenticator<Headers extends object> {
  type Authorizer (line 287) | interface Authorizer<
  type Resender (line 307) | interface Resender<
  type Processor (line 327) | interface Processor<
  type ActionFinally (line 347) | interface ActionFinally<
  type ChannelFilter (line 367) | interface ChannelFilter<Headers extends object> {
  type ChannelAuthorizer (line 383) | interface ChannelAuthorizer<
  type FilterCreator (line 404) | interface FilterCreator<
  type ChannelLoader (line 425) | interface ChannelLoader<
  type ChannelFinally (line 446) | interface ChannelFinally<
  type ChannelUnsubscribe (line 467) | interface ChannelUnsubscribe<
  type ActionCallbacks (line 479) | type ActionCallbacks<
  type ChannelCallbacks (line 496) | type ChannelCallbacks<
  type ActionReporter (line 520) | interface ActionReporter {
  type SubscriptionReporter (line 525) | interface SubscriptionReporter {
  type CleanReporter (line 530) | interface CleanReporter {
  type AuthenticationReporter (line 534) | interface AuthenticationReporter {
  type ReportersArguments (line 540) | interface ReportersArguments {
  type Reporter (line 598) | interface Reporter {
  type Resend (line 605) | type Resend =
  type Logger (line 620) | interface Logger {
  class BaseServer (line 648) | class BaseServer<

FILE: base-server/index.js
  constant SKIP_PROCESS (line 16) | const SKIP_PROCESS = Symbol('skipProcess')
  constant RESEND_META (line 17) | const RESEND_META = ['channels', 'users', 'clients', 'nodes']
  function optionError (line 19) | function optionError(msg) {
  function wasNot403 (line 26) | async function wasNot403(cb) {
  function normalizeTypeCallbacks (line 38) | function normalizeTypeCallbacks(name, callbacks) {
  function normalizeChannelCallbacks (line 55) | function normalizeChannelCallbacks(pattern, callbacks) {
  function subscriberFilterId (line 85) | function subscriberFilterId(action) {
  class BaseServer (line 89) | class BaseServer {
    method constructor (line 90) | constructor(opts = {}) {
    method addClient (line 410) | addClient(connection) {
    method auth (line 418) | auth(authenticator) {
    method buildUndo (line 422) | buildUndo(action, meta, reason, extra) {
    method channel (line 444) | channel(pattern, callbacks, options = {}) {
    method createContext (line 459) | createContext(action, meta) {
    method debugActionError (line 468) | debugActionError(meta, msg) {
    method debugError (line 477) | debugError(error) {
    method denyAction (line 487) | denyAction(action, meta) {
    method destroy (line 493) | destroy() {
    method finally (line 499) | finally(processor, ctx, action, meta) {
    method getProcessor (line 510) | getProcessor(type) {
    method getRegexProcessor (line 516) | getRegexProcessor(type) {
    method handleClient (line 525) | handleClient(ws, req) {
    method http (line 530) | http(method, url, listener) {
    method internalUnknownType (line 543) | internalUnknownType(action, meta) {
    method internalWrongChannel (line 556) | internalWrongChannel(action, meta) {
    method isBruteforce (line 566) | isBruteforce(ip) {
    method isUseless (line 571) | isUseless(action, meta) {
    method listen (line 585) | async listen() {
    method markAsProcessed (line 648) | markAsProcessed(meta) {
    method on (line 659) | on(event, listener) {
    method otherChannel (line 667) | otherChannel(callbacks) {
    method otherType (line 681) | otherType(callbacks) {
    method performUnsubscribe (line 689) | performUnsubscribe(clientNodeId, action, meta) {
    method process (line 715) | process(action, meta = {}) {
    method processAction (line 735) | async processAction(processor, action, meta, start) {
    method processHttp (line 756) | async processHttp(req, res) {
    method rememberBadAuth (line 782) | rememberBadAuth(ip) {
    method replaceResendShortcuts (line 793) | replaceResendShortcuts(meta) {
    method sendAction (line 812) | async sendAction(action, meta) {
    method sendOnConnect (line 879) | sendOnConnect(loader) {
    method setTimeout (line 883) | setTimeout(callback, ms) {
    method subscribe (line 892) | subscribe(nodeId, channel) {
    method subscribeAction (line 903) | async subscribeAction(action, meta, start) {
    method type (line 1008) | type(name, callbacks, options = {}) {
    method undo (line 1025) | undo(action, meta, reason = 'error', extra = {}) {
    method unknownType (line 1032) | unknownType(action, meta) {
    method unsubscribe (line 1037) | unsubscribe(action, meta) {
    method unsubscribeAction (line 1042) | unsubscribeAction(action, meta) {
    method wrongChannel (line 1054) | wrongChannel(action, meta) {

FILE: base-server/index.test.ts
  constant ROOT (line 24) | const ROOT = join(import.meta.dirname, '..')
  constant DEFAULT_OPTIONS (line 26) | const DEFAULT_OPTIONS = {
  constant CERT (line 30) | const CERT = join(ROOT, 'test/fixtures/cert.pem')
  constant KEY (line 31) | const KEY = join(ROOT, 'test/fixtures/key.pem')
  function createServer (line 35) | function createServer(
  function createReporter (line 62) | function createReporter(opts: Partial<BaseServerOptions> = {}): {
  function privateMethods (line 83) | function privateMethods(obj: object): any {
  function emit (line 87) | function emit(obj: any, event: string, ...args: any): void {
  function catchError (line 91) | async function catchError(cb: () => Promise<any>): Promise<Error> {
  function calls (line 100) | function calls(fn: Function | undefined): any[][] {
  function called (line 104) | function called(fn: Function | undefined): boolean {
  function callCount (line 108) | function callCount(fn: Function | undefined): number {
  method process (line 594) | async process(ctx, action, meta) {
  method process (line 623) | async process(ctx, action, meta) {
  method process (line 669) | process() {
  method process (line 702) | process() {
  method onAdd (line 817) | onAdd() {}
  method access (line 874) | access(ctx, action, meta) {
  method send (line 881) | send() {}
  method onAdd (line 882) | onAdd() {}
  method access (line 909) | async access(ctx) {
  method finally (line 913) | finally() {
  method access (line 946) | access() {
  method access (line 979) | access(ctx, action, meta) {
  method access (line 991) | access() {
  method filter (line 994) | async filter() {
  method access (line 1086) | access() {
  method filter (line 1089) | async filter() {
  method access (line 1151) | access() {
  method filter (line 1156) | filter() {
  method load (line 1159) | load() {
  method load (line 1184) | load() {
  method load (line 1228) | load(ctx, action, meta) {
  method access (line 1285) | access(ctx) {
  method process (line 1444) | process() {
  type ActionA (line 1463) | type ActionA = { aValue: string; type: 'A' }
  method process (line 1470) | process(ctx, action) {

FILE: context/index.d.ts
  class ConnectContext (line 7) | class ConnectContext<Headers extends object = unknown> {
  class Context (line 82) | class Context<
  class ChannelContext (line 126) | class ChannelContext<

FILE: context/index.js
  class Context (line 3) | class Context {
    method constructor (line 4) | constructor(server, meta) {
    method sendBack (line 36) | sendBack(action, meta = {}) {

FILE: context/index.test.ts
  constant FAKE_SERVER (line 8) | const FAKE_SERVER: any = {
  method add (line 17) | add(action: Action, meta: ServerMeta) {
  function createContext (line 28) | function createContext(

FILE: create-http-server/index.js
  constant PEM_PREAMBLE (line 6) | const PEM_PREAMBLE = '-----BEGIN'
  function isPem (line 8) | function isPem(content) {
  function readFrom (line 16) | function readFrom(root, file) {
  function createHttpServer (line 22) | async function createHttpServer(opts) {

FILE: create-reporter/index.js
  constant ERROR_CODES (line 6) | const ERROR_CODES = {
  constant REPORTERS (line 48) | const REPORTERS = {
  function cleanFromKeys (line 175) | function cleanFromKeys(obj, regexp, seen) {
  function createRecord (line 195) | function createRecord(level, details, msg) {
  function createReporter (line 210) | function createReporter(options) {

FILE: create-reporter/index.test.ts
  class MemoryStream (line 8) | class MemoryStream {
    method constructor (line 13) | constructor(flushSync: boolean) {
    method write (line 22) | write(chunk: string): void {
  function clean (line 27) | function clean(str: string): string {
  function check (line 38) | function check(type: string, details?: object): void {
  function createError (line 54) | function createError(name: string, message: string): Error {
  method info (line 391) | info(details: object, msg: string) {

FILE: filter-meta/index.js
  function filterMeta (line 3) | function filterMeta(meta) {

FILE: filtered-node/index.js
  function has (line 3) | function has(array, item) {
  class FilteredNode (line 7) | class FilteredNode extends ServerNode {
    method constructor (line 8) | constructor(client, nodeId, log, connection, options) {
    method syncFilter (line 19) | syncFilter(action, meta) {

FILE: filtered-node/index.test.ts
  type Test (line 6) | type Test = {
  function createTest (line 11) | function createTest(): Test {

FILE: human-formatter/index.js
  constant INDENT (line 6) | const INDENT = '  '
  constant PADDING (line 7) | const PADDING = '        '
  constant SEPARATOR (line 8) | const SEPARATOR = os.EOL + os.EOL
  constant NEXT_LINE (line 9) | const NEXT_LINE = os.EOL === '\n' ? '\r\v' : os.EOL
  constant PARAMS_BLACKLIST (line 11) | const PARAMS_BLACKLIST = {
  constant LABELS (line 27) | const LABELS = {
  constant COLORS (line 35) | const COLORS = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan']
  function formatNow (line 37) | function formatNow() {
  function rightPag (line 48) | function rightPag(str, length) {
  function label (line 54) | function label(type, color, labelBg, labelText, message) {
  function formatName (line 61) | function formatName(key) {
  function shuffledColors (line 70) | function shuffledColors(str) {
  function splitAndColorize (line 87) | function splitAndColorize(partLength, str) {
  function formatNodeId (line 109) | function formatNodeId(nodeId) {
  function formatValue (line 121) | function formatValue(value) {
  function formatObject (line 133) | function formatObject(obj) {
  function formatArray (line 138) | function formatArray(array) {
  function formatActionId (line 143) | function formatActionId(id) {
  function formatParams (line 154) | function formatParams(params, parent) {
  function splitByLength (line 197) | function splitByLength(string, max) {
  function prettyStackTrace (line 211) | function prettyStackTrace(stack, basepath) {
  function humanFormatter (line 233) | function humanFormatter(options) {

FILE: human-formatter/utils.js
  function onceXmur3 (line 1) | function onceXmur3(str) {
  function mulberry32 (line 12) | function mulberry32(a) {

FILE: options-loader/index.js
  function loadOptions (line 5) | function loadOptions(spec, process, env) {
  function gatherCliArgs (line 39) | function gatherCliArgs(argv) {
  function parseValues (line 67) | function parseValues(spec, args) {
  function loadEnv (line 84) | function loadEnv(file) {
  function mapArgs (line 110) | function mapArgs(parsedCliArgs, argsSpec) {
  function composeHelp (line 121) | function composeHelp(spec, argv) {
  function composeEnvName (line 160) | function composeEnvName(prefix, name) {
  function composeCliFullName (line 167) | function composeCliFullName(name) {
  function composeCliAliasName (line 171) | function composeCliAliasName(name) {
  function toKebabCase (line 175) | function toKebabCase(word) {
  function oneOf (line 179) | function oneOf(options, rawValue) {
  function number (line 192) | function number(rawValue) {
  function string (line 205) | function string(rawValue) {

FILE: options-loader/index.test.js
  function fakeProcess (line 8) | function fakeProcess(argv, env = {}) {

FILE: request/index.d.ts
  class ResponseError (line 5) | class ResponseError extends Error {

FILE: request/index.js
  class ResponseError (line 1) | class ResponseError extends Error {
    method constructor (line 2) | constructor(statusCode, url) {

FILE: server-client/index.d.ts
  class ServerClient (line 12) | class ServerClient {

FILE: server-client/index.js
  function onSend (line 10) | async function onSend(action, meta) {
  function reportDetails (line 14) | function reportDetails(client) {
  function denyBack (line 22) | function denyBack(app, clientId, action, meta) {
  function queueWorker (line 30) | async function queueWorker(task, next) {
  class ServerClient (line 67) | class ServerClient {
    method constructor (line 68) | constructor(app, connection, key) {
    method auth (line 128) | async auth(nodeId, token) {
    method destroy (line 209) | destroy() {
    method onReceive (line 242) | onReceive(action, meta) {

FILE: server-client/index.test.ts
  function privateMethods (line 27) | function privateMethods(obj: object): any {
  function getPair (line 31) | function getPair(client: ServerClient): TestPair {
  function sendTo (line 35) | async function sendTo(client: ServerClient, msg: Message): Promise<void> {
  function connect (line 41) | async function connect(
  function createConnection (line 57) | function createConnection(): ServerConnection {
  function createServer (line 70) | function createServer(
  function createReporter (line 93) | function createReporter(opts: Partial<BaseServerOptions> = {}): {
  function createClient (line 109) | function createClient(app: BaseServer): ServerClient {
  function connectClient (line 117) | async function connectClient(
  function sent (line 127) | function sent(client: ServerClient): Message[] {
  function sentNames (line 131) | function sentNames(client: ServerClient): string[] {
  function actions (line 135) | function actions(client: ServerClient): Action[] {
  function connectNext (line 376) | async function connectNext(num: number): Promise<void> {
  method finally (line 564) | finally() {
  type FooAction (line 723) | type FooAction = {
  method access (line 728) | async access(ctx, action, meta) {
  method process (line 779) | process(ctx) {
  method access (line 797) | access() {
  method finally (line 800) | finally() {
  method resend (line 842) | resend(ctx, action, meta) {
  method resend (line 898) | resend() {
  method resend (line 904) | resend() {
  method access (line 1240) | access() {
  method process (line 1244) | process() {
  method access (line 1264) | access(ctx, action, meta) {
  method process (line 1269) | process() {
  method access (line 1289) | access(ctx, action, meta) {
  method finally (line 1333) | finally() {
  method finally (line 1343) | finally() {
  method finally (line 1354) | finally() {
  method resend (line 1357) | resend() {
  method access (line 1366) | access() {
  method finally (line 1369) | finally() {
  method finally (line 1379) | finally() {
  method process (line 1383) | process() {
  method access (line 1473) | access(ctx) {
  method finally (line 1477) | finally(ctx) {
  method process (line 1480) | process(ctx) {
  method load (line 1533) | async load(ctx) {
  function meta (line 1546) | function meta(time: number): object {
  method load (line 1561) | load() {
  method load (line 1567) | load() {
  method load (line 1573) | load() {
  function meta (line 1598) | function meta(time: number): object {
  method load (line 1619) | load() {
  method process (line 1628) | process(ctx, action) {
  method resend (line 1631) | resend(ctx, action) {
  method load (line 1665) | load() {
  method process (line 1671) | process(ctx, action, meta) {
  method accessAndProcess (line 1700) | async accessAndProcess(ctx, action, meta) {
  method accessAndProcess (line 1707) | async accessAndProcess(ctx, action, meta) {
  method accessAndLoad (line 1716) | async accessAndLoad(ctx, action, meta) {
  method accessAndLoad (line 1723) | accessAndLoad(ctx, action, meta) {
  method accessAndProcess (line 1781) | async accessAndProcess(ctx) {
  method accessAndProcess (line 1786) | async accessAndProcess(ctx, action) {
  method accessAndProcess (line 1833) | accessAndProcess() {
  method accessAndProcess (line 1838) | accessAndProcess() {
  method accessAndProcess (line 1843) | async accessAndProcess() {
  method accessAndLoad (line 1908) | accessAndLoad() {
  method accessAndLoad (line 1913) | accessAndLoad() {
  method accessAndLoad (line 1918) | accessAndLoad() {
  method accessAndLoad (line 1923) | accessAndLoad() {
  method accessAndLoad (line 2000) | accessAndLoad() {
  method process (line 2035) | process() {
  method process (line 2045) | async process() {
  method process (line 2057) | process() {
  method process (line 2067) | process() {
  method process (line 2170) | process() {
  method process (line 2181) | async process() {
  method process (line 2192) | process() {
  method access (line 2220) | async access() {
  method process (line 2225) | async process() {
  method resend (line 2229) | async resend() {
  method access (line 2236) | async access() {
  method process (line 2240) | async process() {
  method resend (line 2243) | async resend() {
  method access (line 2318) | async access() {
  method process (line 2321) | async process() {
  method access (line 2331) | async access() {
  method process (line 2334) | async process() {
  method access (line 2344) | async access() {
  method process (line 2348) | async process() {
  method access (line 2357) | async access() {
  method process (line 2360) | async process() {
  method access (line 2367) | async access() {
  method process (line 2370) | async process() {
  class OtherNode (line 2478) | class OtherNode extends FilteredNode {
    method syncSinceQuery (line 2479) | syncSinceQuery(): { added: number; entries: [Action, Meta][] } {

FILE: server/errors.ts
  class User (line 19) | class User {
    method constructor (line 23) | constructor(id: string) {
    method save (line 28) | async save(): Promise<void> {}
  type UserRenameAction (line 31) | type UserRenameAction = Action & {
  type UserSubscribeAction (line 37) | type UserSubscribeAction = LoguxSubscribeAction & {
  type UserData (line 41) | type UserData = {
  type UserParams (line 45) | type UserParams = {
  type BadParams (line 49) | type BadParams = number
  method access (line 52) | access(ctx, action) {
  method resend (line 58) | resend(_, action) {
  method process (line 64) | async process(ctx, action) {
  method process (line 76) | async process(_, action) {
  method access (line 85) | access() {
  method access (line 91) | access() {
  method filter (line 95) | async filter(_, action) {
  method load (line 106) | async load(ctx) {
  method access (line 116) | access(ctx, action, meta) {
  method access (line 125) | access() {
  method access (line 135) | access(ctx, action) {

FILE: server/index.d.ts
  type LogStream (line 7) | interface LogStream {
  type LoggerOptions (line 16) | interface LoggerOptions {
  type ServerOptions (line 33) | interface ServerOptions extends BaseServerOptions {
  class Server (line 64) | class Server<

FILE: server/index.js
  class Server (line 48) | class Server extends BaseServer {
    method constructor (line 49) | constructor(opts = {}) {
    method loadOptions (line 96) | static loadOptions(process, defaults) {
    method autoloadModules (line 119) | async autoloadModules(
    method listen (line 148) | async listen(...args) {

FILE: server/index.test.ts
  constant ROOT (line 8) | const ROOT = join(import.meta.dirname, '..')
  constant DATE (line 9) | const DATE = /\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/g
  function start (line 13) | function start(name: string, args?: string[]): Promise<void> {
  function check (line 28) | function check(
  function fakeProcess (line 68) | function fakeProcess(argv: string[] = [], env: object = {}): any {
  function checkOut (line 72) | async function checkOut(
  function checkError (line 86) | async function checkError(

FILE: server/types.ts
  class User (line 17) | class User {
    method constructor (line 21) | constructor(id: string) {
    method save (line 26) | async save(): Promise<void> {}
  type UserRenameAction (line 29) | type UserRenameAction = {
  type UserSubscribeAction (line 35) | type UserSubscribeAction = {
  type UserData (line 39) | type UserData = {
  type UserParams (line 43) | type UserParams = {
  method access (line 48) | access(ctx, action, meta) {
  method process (line 54) | async process(ctx, action) {
  method resend (line 59) | resend(ctx, action) {
  method access (line 67) | access(ctx, action, meta) {
  method filter (line 72) | filter(ctx, action) {
  method load (line 83) | async load(ctx) {
  method access (line 98) | access(ctx, action, meta) {
  method access (line 113) | access(ctx, action) {

FILE: test-client/index.d.ts
  class LoguxActionError (line 16) | class LoguxActionError extends Error {
  type TestClientOptions (line 20) | interface TestClientOptions {
  class TestClient (line 69) | class TestClient {

FILE: test-client/index.js
  class TestClient (line 7) | class TestClient {
    method constructor (line 8) | constructor(server, userId, opts = {}) {
    method collect (line 46) | async collect(test) {
    method connect (line 58) | connect() {
    method disconnect (line 79) | disconnect() {
    method process (line 84) | process(action, meta) {
    method received (line 136) | async received(test) {
    method subscribe (line 149) | async subscribe(channel, filter, since) {
    method unsubscribe (line 164) | unsubscribe(channel, filter) {

FILE: test-client/index.test.ts
  function catchError (line 14) | async function catchError(cb: () => Promise<any>): Promise<LoguxActionEr...
  function privateMethods (line 25) | function privateMethods(obj: object): any {
  method process (line 47) | process(ctx) {
  method process (line 95) | process() {
  method process (line 104) | process(ctx, action, meta) {
  method load (line 200) | load(ctx, action) {
  method write (line 249) | write() {}
  method process (line 317) | process(ctx) {

FILE: test-server/index.d.ts
  type TestServerOptions (line 12) | interface TestServerOptions extends Omit<
  class TestServer (line 50) | class TestServer<

FILE: test-server/index.js
  class TestServer (line 8) | class TestServer extends BaseServer {
    method constructor (line 9) | constructor(opts = {}) {
    method connect (line 42) | async connect(userId, opts = {}) {
    method expectDenied (line 48) | async expectDenied(test) {
    method expectError (line 52) | async expectError(text, test) {
    method expectUndo (line 66) | async expectUndo(reason, test) {
    method expectWrongCredentials (line 85) | async expectWrongCredentials(userId, opts = {}) {
    method fetch (line 96) | async fetch(path, init) {
    method isBruteforce (line 108) | isBruteforce() {
Condensed preview — 86 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (390K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".github/workflows/api.yml",
    "chars": 443,
    "preview": "name: Update API\non:\n  create:\n    tags:\n      - '*.*.*'\npermissions: {}\njobs:\n  api:\n    runs-on: ubuntu-latest\n    ste"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1479,
    "preview": "name: Release\non:\n  push:\n    tags:\n      - '*'\npermissions:\n  contents: write\njobs:\n  release:\n    name: Release On Tag"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1561,
    "preview": "name: Test\non:\n  push:\n    branches:\n      - main\n      - next\n  pull_request:\npermissions:\n  contents: read\njobs:\n  ful"
  },
  {
    "path": ".gitignore",
    "chars": 25,
    "preview": "node_modules/\n\ncoverage/\n"
  },
  {
    "path": ".npmignore",
    "chars": 83,
    "preview": "test/\ncoverage/\ntsconfig.json\n**/*.test.ts\n**/types.ts\n**/errors.ts\n\n__snapshots__\n"
  },
  {
    "path": ".prettierrc.js",
    "chars": 87,
    "preview": "import loguxOxfmtConfig from '@logux/oxc-configs/fmt'\n\nexport default loguxOxfmtConfig\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 10427,
    "preview": "# Change Log\n\nThis project adheres to [Semantic Versioning](http://semver.org/).\n\n## 0.14 “Sliver of Straw”\n\n- Removed N"
  },
  {
    "path": "LICENSE",
    "chars": 1095,
    "preview": "The MIT License (MIT)\n\nCopyright 2016 Andrey Sitnik <andrey@sitnik.es>\n\nPermission is hereby granted, free of charge, to"
  },
  {
    "path": "README.md",
    "chars": 2158,
    "preview": "# Logux Server [![Cult Of Martians][cult-img]][cult]\n\n<img align=\"right\" width=\"95\" height=\"148\" title=\"Logux logotype\"\n"
  },
  {
    "path": "add-http-pages/hello.html",
    "chars": 6644,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Logux Server</title>\n    <style>\n      html {\n  "
  },
  {
    "path": "add-http-pages/index.js",
    "chars": 633,
    "preview": "import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nlet hello\nasync function readHello() {\n  i"
  },
  {
    "path": "add-http-pages/index.test.ts",
    "chars": 5758,
    "preview": "import { type TestLog, TestTime } from '@logux/core'\nimport http from 'node:http'\nimport { setTimeout } from 'node:timer"
  },
  {
    "path": "add-sync-map/index.d.ts",
    "chars": 6156,
    "preview": "import type {\n  LoguxSubscribeAction,\n  SyncMapChangeAction,\n  SyncMapChangedAction,\n  SyncMapCreateAction,\n  SyncMapCre"
  },
  {
    "path": "add-sync-map/index.js",
    "chars": 5444,
    "preview": "const WITH_TIME = Symbol('WITH_TIME')\n\nexport function ChangedAt(value, time) {\n  return { time, value, [WITH_TIME]: tru"
  },
  {
    "path": "add-sync-map/index.test.ts",
    "chars": 13271,
    "preview": "import {\n  defineSyncMapActions,\n  LoguxNotFoundError,\n  loguxProcessed,\n  loguxSubscribed\n} from '@logux/actions'\nimpor"
  },
  {
    "path": "allowed-meta/index.d.ts",
    "chars": 373,
    "preview": "/**\n * List of meta keys permitted for clients.\n *\n *```js\n * import { ALLOWED_META } from '@logux/server'\n * async func"
  },
  {
    "path": "allowed-meta/index.js",
    "chars": 58,
    "preview": "export const ALLOWED_META = ['id', 'time', 'subprotocol']\n"
  },
  {
    "path": "allowed-meta/index.test.ts",
    "chars": 204,
    "preview": "import { expect, it } from 'vitest'\n\nimport { ALLOWED_META } from '../index.js'\n\nit('has allowed meta keys list', () => "
  },
  {
    "path": "base-server/index.d.ts",
    "chars": 30257,
    "preview": "import type {\n  AbstractActionCreator,\n  LoguxSubscribeAction,\n  LoguxUnsubscribeAction\n} from '@logux/actions'\nimport t"
  },
  {
    "path": "base-server/index.js",
    "chars": 30175,
    "preview": "import { LoguxNotFoundError } from '@logux/actions'\nimport { Log, MemoryStore, parseId, ServerConnection } from '@logux/"
  },
  {
    "path": "base-server/index.test.ts",
    "chars": 38656,
    "preview": "import { defineAction } from '@logux/actions'\nimport {\n  type Action,\n  Log,\n  MemoryStore,\n  type TestLog,\n  TestTime\n}"
  },
  {
    "path": "context/index.d.ts",
    "chars": 3077,
    "preview": "import type { AnyAction } from '@logux/core'\n\nimport type { ServerMeta } from '../base-server/index.js'\nimport type { Se"
  },
  {
    "path": "context/index.js",
    "chars": 1052,
    "preview": "import { parseId } from '@logux/core'\n\nexport class Context {\n  constructor(server, meta) {\n    this.server = server\n   "
  },
  {
    "path": "context/index.test.ts",
    "chars": 2143,
    "preview": "import type { Action } from '@logux/core'\nimport { beforeEach, expect, it } from 'vitest'\n\nimport { Context, type Server"
  },
  {
    "path": "create-http-server/index.js",
    "chars": 1024,
    "preview": "import { promises as fs } from 'node:fs'\nimport http from 'node:http'\nimport https from 'node:https'\nimport { isAbsolute"
  },
  {
    "path": "create-reporter/__snapshots__/index.test.ts.snap",
    "chars": 23386,
    "preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`allows custom loggers 1`] = `\"{\"details\":{\"conne"
  },
  {
    "path": "create-reporter/index.js",
    "chars": 7047,
    "preview": "import os from 'node:os'\nimport { sep } from 'node:path'\n\nimport humanFormatter from '../human-formatter/index.js'\n\ncons"
  },
  {
    "path": "create-reporter/index.test.ts",
    "chars": 9083,
    "preview": "import '../test/force-colors.js'\n\nimport { LoguxError } from '@logux/core'\nimport { describe, expect, it } from 'vitest'"
  },
  {
    "path": "filter-meta/index.d.ts",
    "chars": 241,
    "preview": "import type { ServerMeta } from '../base-server/index.js'\n\n/**\n * Remove all non-allowed keys from meta.\n *\n * @param me"
  },
  {
    "path": "filter-meta/index.js",
    "chars": 224,
    "preview": "import { ALLOWED_META } from '../allowed-meta/index.js'\n\nexport function filterMeta(meta) {\n  let result = {}\n  for (let"
  },
  {
    "path": "filter-meta/index.test.ts",
    "chars": 509,
    "preview": "import { expect, it } from 'vitest'\n\nimport { filterMeta, type ServerMeta } from '../index.js'\n\nit('filters meta', () =>"
  },
  {
    "path": "filtered-node/index.js",
    "chars": 654,
    "preview": "import { ServerNode } from '@logux/core'\n\nfunction has(array, item) {\n  return array && array.includes(item)\n}\n\nexport c"
  },
  {
    "path": "filtered-node/index.test.ts",
    "chars": 2790,
    "preview": "import { ClientNode, type TestLog, TestPair, TestTime } from '@logux/core'\nimport { afterEach, expect, it } from 'vitest"
  },
  {
    "path": "human-formatter/index.js",
    "chars": 7699,
    "preview": "import os from 'node:os'\nimport { stripVTControlCharacters, styleText } from 'node:util'\n\nimport { mulberry32, onceXmur3"
  },
  {
    "path": "human-formatter/utils.js",
    "chars": 565,
    "preview": "export function onceXmur3(str) {\n  let h = 1779033703 ^ str.length\n  for (let i = 0; i < str.length; i++) {\n    h = Math"
  },
  {
    "path": "index.d.ts",
    "chars": 848,
    "preview": "export {\n  addSyncMap,\n  addSyncMapFilter,\n  ChangedAt,\n  NoConflictResolution,\n  SyncMapData,\n  WithoutTime,\n  WithTime"
  },
  {
    "path": "index.js",
    "chars": 525,
    "preview": "export {\n  addSyncMap,\n  addSyncMapFilter,\n  ChangedAt,\n  NoConflictResolution\n} from './add-sync-map/index.js'\nexport {"
  },
  {
    "path": "options-loader/__snapshots__/index.test.js.snap",
    "chars": 890,
    "preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`loadOptions > returns help 1`] = `\n\"Start Logux "
  },
  {
    "path": "options-loader/index.js",
    "chars": 5294,
    "preview": "import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { styleText } from 'node:util"
  },
  {
    "path": "options-loader/index.test.js",
    "chars": 5501,
    "preview": "import '../test/force-colors.js'\n\nimport { resolve } from 'node:path'\nimport { describe, expect, it } from 'vitest'\n\nimp"
  },
  {
    "path": "options-loader/test.env",
    "chars": 70,
    "preview": "# Comment\n\nDATABASE_URL=postgresql://localhost/logux\nLOGUX_PORT=31337\n"
  },
  {
    "path": "oxfmt.config.ts",
    "chars": 87,
    "preview": "import loguxOxfmtConfig from '@logux/oxc-configs/fmt'\n\nexport default loguxOxfmtConfig\n"
  },
  {
    "path": "oxlint.config.ts",
    "chars": 671,
    "preview": "import loguxOxlintConfig from '@logux/oxc-configs/lint'\nimport { defineConfig } from 'oxlint'\n\nexport default defineConf"
  },
  {
    "path": "package.json",
    "chars": 1555,
    "preview": "{\n  \"name\": \"@logux/server\",\n  \"version\": \"0.14.0\",\n  \"description\": \"Build own Logux server\",\n  \"keywords\": [\n    \"coll"
  },
  {
    "path": "request/index.d.ts",
    "chars": 235,
    "preview": "/**\n * Throwing this error in `accessAndProcess` or `accessAndLoad`\n * will deny the action.\n */\nexport class ResponseEr"
  },
  {
    "path": "request/index.js",
    "chars": 193,
    "preview": "export class ResponseError extends Error {\n  constructor(statusCode, url) {\n    super(`${statusCode} response on ${url}`"
  },
  {
    "path": "server/__snapshots__/index.test.ts.snap",
    "chars": 7477,
    "preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`destroys everything on exit 1`] = `\n\" INFO   Log"
  },
  {
    "path": "server/errors.ts",
    "chars": 3088,
    "preview": "import { LoguxSubscribeAction, defineAction } from '@logux/actions'\n\nimport { Server, Action } from '../index.js'\n\nlet s"
  },
  {
    "path": "server/index.d.ts",
    "chars": 2365,
    "preview": "import {\n  BaseServer,\n  type BaseServerOptions,\n  type Logger\n} from '../base-server/index.js'\n\nexport interface LogStr"
  },
  {
    "path": "server/index.js",
    "chars": 3749,
    "preview": "import { join, relative } from 'node:path'\nimport { styleText } from 'node:util'\nimport { glob } from 'tinyglobby'\n\nimpo"
  },
  {
    "path": "server/index.test.ts",
    "chars": 6585,
    "preview": "import spawn from 'cross-spawn'\nimport type { ChildProcess, SpawnOptions } from 'node:child_process'\nimport { join } fro"
  },
  {
    "path": "server/types.ts",
    "chars": 2293,
    "preview": "import { defineAction, type LoguxSubscribeAction } from '@logux/actions'\n\nimport { type Action, Server } from '../index."
  },
  {
    "path": "server-client/index.d.ts",
    "chars": 1974,
    "preview": "import type { ServerConnection, ServerNode } from '@logux/core'\n\nimport type { BaseServer } from '../base-server/index.j"
  },
  {
    "path": "server-client/index.js",
    "chars": 8912,
    "preview": "import { LoguxError, parseId } from '@logux/core'\nimport cookie from 'cookie'\nimport fastq from 'fastq'\n\nimport { ALLOWE"
  },
  {
    "path": "server-client/index.test.ts",
    "chars": 61516,
    "preview": "import { LoguxNotFoundError } from '@logux/actions'\nimport {\n  type Action,\n  LoguxError,\n  type Message,\n  type Meta,\n "
  },
  {
    "path": "test/fixtures/cert.pem",
    "chars": 1424,
    "preview": "-----BEGIN CERTIFICATE-----\nMIID7jCCAtagAwIBAgIUO6cuMpNPGoG1mUO5WcSQMzvNaHkwDQYJKoZIhvcNAQEL\nBQAwZjELMAkGA1UEBhMCVVMxCzA"
  },
  {
    "path": "test/fixtures/key.pem",
    "chars": 1675,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAzD7FPEH7miQ8CZDPI2OWgPF5CXzqojns1bPkEhhEPrGEBtRk\nYEThFpa0+Z1XD61t++t2yoA"
  },
  {
    "path": "test/force-colors.js",
    "chars": 30,
    "preview": "process.env.FORCE_COLOR = '1'\n"
  },
  {
    "path": "test/servers/autoload-error-modules.js",
    "chars": 312,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  root: import"
  },
  {
    "path": "test/servers/autoload-modules.js",
    "chars": 286,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  root: import"
  },
  {
    "path": "test/servers/destroy.js",
    "chars": 498,
    "preview": "#!/usr/bin/env node\n\nimport { setTimeout } from 'node:timers/promises'\n\nimport { Server } from '../../index.js'\n\nlet app"
  },
  {
    "path": "test/servers/eacces.js",
    "chars": 219,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  port: 1000,\n"
  },
  {
    "path": "test/servers/eaddrinuse.js",
    "chars": 274,
    "preview": "#!/usr/bin/env node\n\nimport os from 'node:os'\n\nimport { Server } from '../../index.js'\n\nos.platform = () => 'linux'\n\nlet"
  },
  {
    "path": "test/servers/error-modules/wrond-export/index.js",
    "chars": 37,
    "preview": "export default 'wrong module export'\n"
  },
  {
    "path": "test/servers/json.js",
    "chars": 237,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  logger: 'json',\n  minSubprotocol:"
  },
  {
    "path": "test/servers/logger.js",
    "chars": 357,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    "
  },
  {
    "path": "test/servers/missed.js",
    "chars": 244,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    "
  },
  {
    "path": "test/servers/modules/child/index.foo.js",
    "chars": 143,
    "preview": "setTimeout(() => {\n  let error = new Error('Test Error')\n  error.stack = `${error.stack.split('\\n')[0]}\\nfake stacktrace"
  },
  {
    "path": "test/servers/modules/child/index.js",
    "chars": 176,
    "preview": "import { setTimeout } from 'node:timers/promises'\n\nexport default async server => {\n  await setTimeout(100)\n  console.lo"
  },
  {
    "path": "test/servers/modules/child/lib/lib.js",
    "chars": 143,
    "preview": "setTimeout(() => {\n  let error = new Error('Test Error')\n  error.stack = `${error.stack.split('\\n')[0]}\\nfake stacktrace"
  },
  {
    "path": "test/servers/modules/root.js",
    "chars": 94,
    "preview": "export default server => {\n  console.log(`Root path module: ${server.options.subprotocol}`)\n}\n"
  },
  {
    "path": "test/servers/modules/root.test.js",
    "chars": 27,
    "preview": "throw new Error('No load')\n"
  },
  {
    "path": "test/servers/options.js",
    "chars": 267,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    "
  },
  {
    "path": "test/servers/root.js",
    "chars": 356,
    "preview": "#!/usr/bin/env node\n\nimport { join } from 'node:path'\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  "
  },
  {
    "path": "test/servers/throw.js",
    "chars": 369,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  subprotocol:"
  },
  {
    "path": "test/servers/unbind.js",
    "chars": 207,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  subprotocol:"
  },
  {
    "path": "test/servers/uncatch.js",
    "chars": 425,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server({\n  minSubprotocol: 1,\n  subprotocol:"
  },
  {
    "path": "test/servers/unknown.js",
    "chars": 267,
    "preview": "#!/usr/bin/env node\n\nimport { Server } from '../../index.js'\n\nlet app = new Server(\n  Server.loadOptions(process, {\n    "
  },
  {
    "path": "test-client/index.d.ts",
    "chars": 5447,
    "preview": "import type {\n  LoguxSubscribeAction,\n  LoguxUnsubscribeAction\n} from '@logux/actions'\nimport type {\n  Action,\n  AnyActi"
  },
  {
    "path": "test-client/index.js",
    "chars": 4703,
    "preview": "import { ClientNode, TestPair } from '@logux/core'\nimport cookie from 'cookie'\nimport { setTimeout } from 'node:timers/p"
  },
  {
    "path": "test-client/index.test.ts",
    "chars": 11221,
    "preview": "import { TestTime } from '@logux/core'\nimport { restoreAll, type Spy, spyOn } from 'nanospy'\nimport { setTimeout } from "
  },
  {
    "path": "test-server/index.d.ts",
    "chars": 3563,
    "preview": "import type { TestLog, TestTime } from '@logux/core'\n\nimport { BaseServer } from '../base-server/index.js'\nimport type {"
  },
  {
    "path": "test-server/index.js",
    "chars": 2579,
    "preview": "import { TestTime } from '@logux/core'\nimport { createServer } from 'node:http'\n\nimport { BaseServer } from '../base-ser"
  },
  {
    "path": "tsconfig.json",
    "chars": 263,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2024\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esMo"
  },
  {
    "path": "vite.config.ts",
    "chars": 489,
    "preview": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      exclude: [\n "
  }
]

About this extraction

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

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

Copied to clipboard!