Community

Contributing

How to contribute to IOServer — development setup, architecture rules, test suite, and release process.

Contributing

Prerequisites

  • Node.js ≥ 18 (≥ 20 recommended)
  • pnpm v9 (npm install -g pnpm@9)
  • TypeScript ≥ 5.0

Development setup

git clone https://github.com/x42en/IOServer.git
cd IOServer
pnpm install
pnpm run build       # compile src/ → dist/
pnpm run dev:simple  # run the simple example
pnpm run dev:chat    # run the full chat example

Architecture rules

IOServer enforces a strict component model. Follow these rules when writing code or reviewing pull requests.

Separation of concerns

ComponentAllowed toMust NOT
ManagerHold shared state, interact with external services, expose typed public APIHandle HTTP/WS directly, know about sockets
ServiceHandle Socket.IO events, access managers via appHandle, emit/broadcastDirectly call Fastify, import http libs
ControllerHandle Fastify requests, access managers via appHandle, return responsesHold persistent state, access sockets
WatcherRun background loops, access managers, push WS events via appHandle.sendHandle HTTP/WS client connections directly
MiddlewareIntercept requests/connections, read from managers, set request/socket propertiesHold state between requests

Registration order

Managers must be registered before any component that uses them:

// ✅ Correct
server.addManager({ name: "db", manager: DatabaseManager });
server.addService({ name: "chat", service: ChatService }); // can use appHandle.db

// ❌ Wrong
server.addService({ name: "chat", service: ChatService }); // appHandle.db is undefined!
server.addManager({ name: "db", manager: DatabaseManager });

TypeScript requirements

  • strict: true — no exceptions
  • No any in public method signatures unless the value is genuinely untyped (e.g. Socket.IO payloads)
  • Use unknown + type guards instead of any wherever possible
  • Define interfaces for data shapes in services and managers
  • Watcher interval IDs: use ReturnType<typeof setInterval> instead of NodeJS.Timeout for portability

Naming conventions

  • Private/internal service methods: prefix with _ (prevents Socket.IO event registration)
  • Managers in appHandle: camelCase (e.g. sessionManager, db)
  • Route files: match controller name exactly (e.g. ApiControllerroutes/api.json)

Test suite

Structure

tests/
├── setup.ts                            # Global test config (timeout, console suppression)
├── unit/
│   ├── IOServer.test.ts               # Server init, registration, logging, duplicates
│   ├── IOServer.static.test.ts        # rootDir, spaFallback
│   ├── BaseClasses.test.ts            # All base class instantiation
│   └── IOServerError.test.ts          # Error creation, statusCode, instanceof
├── integration/
│   └── IOServer.integration.test.ts   # HTTP routes, WebSocket connect/emit, CORS, 404
├── e2e/
│   └── chat-app.e2e.test.ts          # Full chat app flow
└── performance/
    └── performance.test.ts            # 50 concurrent connections, memory leak check

Running tests

pnpm test                          # all tests
pnpm run test:unit                 # unit tests only
pnpm run test:integration          # integration tests only
pnpm run test:e2e                  # end-to-end tests only
pnpm run test:performance          # performance tests
pnpm run test:coverage             # coverage report
pnpm run test:watch                # watch mode

Coverage targets

MetricTarget
Statements> 90%
Branches> 85%
Functions> 95%
Lines> 90%

Writing tests

  • Unit tests must not start a real server (mock or use in-memory configurations)
  • Integration tests may bind to ports in the 3001–3020 range
  • E2E tests use port 3004
  • Performance tests use port 3005
  • Each test file must close its server in afterAll / afterEach

Linting

pnpm run lint        # report issues
pnpm run lint:fix    # auto-fix

ESLint configuration is at eslint.config.js. Lint errors must be resolved before merging — the CI pipeline runs lint with continue-on-error: true for now, but warnings should not accumulate.

Commit conventions

Use Conventional Commits:

feat: add BaseService._onConnect lifecycle hook
fix: handle missing callback in service error path
docs: update BaseMiddleware WebSocket example
test: add integration test for SPA fallback
refactor: extract route prefix logic to helper
chore: bump fastify to 5.8

Types: feat, fix, docs, test, refactor, perf, chore, ci.

Breaking changes: append ! and add a BREAKING CHANGE: footer.

Pull requests

  1. Fork the repository and create a feature branch (feat/my-feature)
  2. Write or update tests for your change
  3. Ensure pnpm test passes (all four test suites)
  4. Ensure pnpm run build produces no TypeScript errors
  5. Open a PR against main — CI runs on every PR

Release process

Releases are cut by repository maintainers:

  1. Update version in package.json and commit (chore: release vX.Y.Z)
  2. Push the commit to main
  3. Create a GitHub Release named vX.Y.Z — this triggers the publish.yml workflow
  4. The workflow builds, tests, and publishes to npm and GitHub Packages automatically

Pre-releases: tags containing - (e.g. v3.0.0-beta.1) are automatically marked as pre-release on GitHub.

Copyright © 2026