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
| Component | Allowed to | Must NOT |
|---|---|---|
| Manager | Hold shared state, interact with external services, expose typed public API | Handle HTTP/WS directly, know about sockets |
| Service | Handle Socket.IO events, access managers via appHandle, emit/broadcast | Directly call Fastify, import http libs |
| Controller | Handle Fastify requests, access managers via appHandle, return responses | Hold persistent state, access sockets |
| Watcher | Run background loops, access managers, push WS events via appHandle.send | Handle HTTP/WS client connections directly |
| Middleware | Intercept requests/connections, read from managers, set request/socket properties | Hold 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
anyin public method signatures unless the value is genuinely untyped (e.g. Socket.IO payloads) - Use
unknown+ type guards instead ofanywherever possible - Define interfaces for data shapes in services and managers
- Watcher interval IDs: use
ReturnType<typeof setInterval>instead ofNodeJS.Timeoutfor 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.
ApiController→routes/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
| Metric | Target |
|---|---|
| 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
- Fork the repository and create a feature branch (
feat/my-feature) - Write or update tests for your change
- Ensure
pnpm testpasses (all four test suites) - Ensure
pnpm run buildproduces no TypeScript errors - Open a PR against
main— CI runs on every PR
Release process
Releases are cut by repository maintainers:
- Update
versioninpackage.jsonand commit (chore: release vX.Y.Z) - Push the commit to
main - Create a GitHub Release named
vX.Y.Z— this triggers thepublish.ymlworkflow - 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.