onilib
Version:
A modular Node.js library for real-time online integration in games and web applications
1,001 lines (844 loc) โข 27.2 kB
JavaScript
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const { promisify } = require('util');
const writeFile = promisify(fs.writeFile);
const mkdir = promisify(fs.mkdir);
const access = promisify(fs.access);
class NoiCLI {
constructor() {
this.commands = {
init: this.init.bind(this),
start: this.start.bind(this),
test: this.test.bind(this),
build: this.build.bind(this),
help: this.help.bind(this)
};
}
async run() {
const args = process.argv.slice(2);
const command = args[0] || 'help';
const commandArgs = args.slice(1);
if (!this.commands[command]) {
console.error(`โ Unknown command: ${command}`);
this.help();
process.exit(1);
}
try {
await this.commands[command](commandArgs);
} catch (error) {
console.error(`โ Error executing command '${command}':`, error.message);
process.exit(1);
}
}
async init(args) {
const projectName = args[0] || path.basename(process.cwd());
const projectPath = process.cwd();
console.log(`๐ Initializing ONILib project: ${projectName}`);
// Check if already initialized
try {
await access(path.join(projectPath, 'noi.config.js'));
console.log('โ ๏ธ Project already initialized (noi.config.js exists)');
return;
} catch {
// File doesn't exist, continue with initialization
}
// Create directory structure
const directories = [
'src',
'src/modules',
'test',
'examples',
'data'
];
for (const dir of directories) {
const dirPath = path.join(projectPath, dir);
try {
await mkdir(dirPath, { recursive: true });
console.log(`๐ Created directory: ${dir}`);
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
}
// Create configuration file
await this.createConfigFile(projectPath, projectName);
// Create main application file
await this.createMainFile(projectPath);
// Create example client
await this.createExampleClient(projectPath);
// Create package.json if it doesn't exist
await this.createPackageJson(projectPath, projectName);
// Create README
await this.createReadme(projectPath, projectName);
// Create .gitignore
await this.createGitignore(projectPath);
console.log('โ
Project initialized successfully!');
console.log('\n๐ Next steps:');
console.log(' 1. npm install');
console.log(' 2. onilib start');
console.log(' 3. Open examples/client.html in your browser');
}
async createConfigFile(projectPath, projectName) {
const configContent = `// ONILib Configuration
module.exports = {
// Application settings
name: '${projectName}',
environment: process.env.NODE_ENV || 'development',
// Server configuration
port: process.env.PORT || 3000,
host: process.env.HOST || '0.0.0.0',
// Authentication
auth: {
jwtSecret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
jwtExpiresIn: '24h',
apiKeys: [
process.env.API_KEY || 'noi_default_api_key_change_me'
]
},
// WebSocket/Realtime
realtime: {
port: process.env.WS_PORT || 8080,
maxConnections: 1000,
heartbeatInterval: 30000,
authRequired: true
},
// Storage
storage: {
type: process.env.STORAGE_TYPE || 'sqlite',
path: process.env.STORAGE_PATH || './data/app.db'
},
// Matchmaking
matchmaking: {
maxPlayersPerMatch: 2,
matchTimeout: 30000,
queueTimeout: 300000,
enableSkillMatching: false
},
// P2P/WebRTC
p2p: {
enableRelay: false,
maxPeersPerRoom: 8,
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
},
// Admin panel
admin: {
port: process.env.ADMIN_PORT || 3001,
authRequired: true,
enableCors: true
}
};
`;
await writeFile(path.join(projectPath, 'noi.config.js'), configContent);
console.log('๐ Created noi.config.js');
}
async createMainFile(projectPath) {
const mainContent = `// Main application entry point
const { NodeOnlineIntegration } = require('onilib');
const fs = require('fs');
const path = require('path');
// Try to load config file, fallback to default config
let config;
try {
config = require('./noi.config.js');
} catch (error) {
console.log('โ ๏ธ Config file not found, using default configuration');
config = {
name: 'onilib-project',
environment: 'development',
port: 3000,
auth: {
jwtSecret: 'default-secret-change-in-production',
jwtExpiresIn: '24h',
apiKeys: ['default-api-key']
},
realtime: {
port: 8080,
maxConnections: 1000,
heartbeatInterval: 30000,
authRequired: false
},
storage: {
type: 'sqlite',
path: './data/app.db'
},
matchmaking: {
maxPlayersPerMatch: 2,
matchTimeout: 30000,
queueTimeout: 300000
},
p2p: {
enableRelay: false,
maxPeersPerRoom: 8,
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
},
admin: {
port: 3001,
authRequired: false,
enableCors: true
}
};
}
async function main() {
// Create NOI instance
const noi = new NodeOnlineIntegration(config);
// Start the server
await noi.start();
// Get modules for custom logic
const realtime = noi.getModule('realtime');
const matchmaking = noi.getModule('matchmaking');
const p2p = noi.getModule('p2p');
// Custom message handlers
realtime.registerHandler('custom_message', (client, message) => {
console.log('Custom message received:', message);
// Echo back to sender
realtime.sendToClient(client, {
type: 'custom_response',
data: {
echo: message.data,
timestamp: Date.now()
}
});
});
// Matchmaking events
matchmaking.on('match:created', (match) => {
console.log('Match created:', match.id);
});
matchmaking.on('match:started', (match) => {
console.log('Match started:', match.id);
// Send game start data to all players
for (const player of match.players) {
realtime.sendToClient(player.client, {
type: 'game_start',
data: {
matchId: match.id,
gameMode: 'default',
players: match.players.map(p => ({ id: p.id, skill: p.skill }))
}
});
}
});
// P2P events
p2p.on('peer:joined_room', ({ peer, roomId }) => {
console.log('Peer joined P2P room:', peer.id, roomId);
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('๐ Shutting down...');
await noi.stop();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('๐ Shutting down...');
await noi.stop();
process.exit(0);
});
}
// Start the application
if (require.main === module) {
main().catch(console.error);
}
module.exports = { main };
`;
await writeFile(path.join(projectPath, 'src', 'index.js'), mainContent);
console.log('๐ Created src/index.js');
}
async createExampleClient(projectPath) {
const clientContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ONILib Example Client</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color:
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-weight: bold;
}
.connected { background-color:
.disconnected { background-color:
.authenticated { background-color:
button {
background-color:
color: white;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 4px;
cursor: pointer;
}
button:hover { background-color:
button:disabled {
background-color:
cursor: not-allowed;
}
.log {
background-color:
border: 1px solid
padding: 10px;
height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
input, select {
padding: 8px;
margin: 5px;
border: 1px solid
border-radius: 4px;
}
.section {
margin: 20px 0;
padding: 15px;
border: 1px solid
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>๐ ONILib - Example Client</h1>
<div class="section">
<h3>Connection</h3>
<div id="status" class="status disconnected">Disconnected</div>
<input type="text" id="wsUrl" value="ws://localhost:8080" placeholder="WebSocket URL">
<input type="text" id="apiKey" value="noi_default_api_key_change_me" placeholder="API Key">
<button id="connectBtn" onclick="connect()">Connect</button>
<button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
</div>
<div class="section">
<h3>Rooms</h3>
<input type="text" id="roomId" value="test-room" placeholder="Room ID">
<button id="joinRoomBtn" onclick="joinRoom()" disabled>Join Room</button>
<button id="leaveRoomBtn" onclick="leaveRoom()" disabled>Leave Room</button>
</div>
<div class="section">
<h3>Matchmaking</h3>
<select id="queueType">
<option value="default">Default Queue</option>
<option value="ranked">Ranked Queue</option>
</select>
<input type="number" id="skillLevel" value="1000" placeholder="Skill Level">
<button id="joinQueueBtn" onclick="joinQueue()" disabled>Join Queue</button>
<button id="leaveQueueBtn" onclick="leaveQueue()" disabled>Leave Queue</button>
</div>
<div class="section">
<h3>P2P</h3>
<input type="text" id="p2pRoomId" value="p2p-room" placeholder="P2P Room ID">
<button id="joinP2PBtn" onclick="joinP2PRoom()" disabled>Join P2P Room</button>
<button id="leaveP2PBtn" onclick="leaveP2PRoom()" disabled>Leave P2P Room</button>
</div>
<div class="section">
<h3>Messages</h3>
<input type="text" id="messageInput" placeholder="Type a message...">
<button onclick="sendMessage()" disabled id="sendBtn">Send Message</button>
</div>
<div class="section">
<h3>Log</h3>
<div id="log" class="log"></div>
<button onclick="clearLog()">Clear Log</button>
</div>
</div>
<script>
let ws = null;
let connected = false;
let authenticated = false;
let currentRoom = null;
let currentQueue = null;
let currentP2PRoom = null;
function log(message) {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
logDiv.innerHTML += \`[\${timestamp}] \${message}\n\`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function updateStatus() {
const statusDiv = document.getElementById('status');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const buttons = ['joinRoomBtn', 'leaveRoomBtn', 'joinQueueBtn', 'leaveQueueBtn', 'joinP2PBtn', 'leaveP2PBtn', 'sendBtn'];
if (connected && authenticated) {
statusDiv.textContent = 'Connected & Authenticated';
statusDiv.className = 'status authenticated';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
buttons.forEach(id => document.getElementById(id).disabled = false);
} else if (connected) {
statusDiv.textContent = 'Connected (Not Authenticated)';
statusDiv.className = 'status connected';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
buttons.forEach(id => document.getElementById(id).disabled = true);
} else {
statusDiv.textContent = 'Disconnected';
statusDiv.className = 'status disconnected';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
buttons.forEach(id => document.getElementById(id).disabled = true);
}
}
function connect() {
const url = document.getElementById('wsUrl').value;
const apiKey = document.getElementById('apiKey').value;
ws = new WebSocket(url);
ws.onopen = () => {
connected = true;
log('๐ Connected to WebSocket');
updateStatus();
// Authenticate with API key
ws.send(JSON.stringify({
type: 'auth',
data: {
strategy: 'apikey',
credentials: { apiKey }
}
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
ws.onclose = () => {
connected = false;
authenticated = false;
log('โ Disconnected from WebSocket');
updateStatus();
};
ws.onerror = (error) => {
log('โ WebSocket error: ' + error);
};
}
function disconnect() {
if (ws) {
ws.close();
}
}
function handleMessage(message) {
log('๐จ Received: ' + JSON.stringify(message, null, 2));
switch (message.type) {
case 'auth_success':
authenticated = true;
log('โ
Authentication successful');
updateStatus();
break;
case 'error':
log('โ Error: ' + message.error);
break;
case 'room_joined':
currentRoom = message.data.roomId;
log('๐ Joined room: ' + currentRoom);
break;
case 'room_left':
log('๐ช Left room: ' + currentRoom);
currentRoom = null;
break;
case 'queue_joined':
currentQueue = message.data.queueType;
log('โณ Joined queue: ' + currentQueue);
break;
case 'queue_left':
log('๐ช Left queue: ' + currentQueue);
currentQueue = null;
break;
case 'match_found':
log('๐ฏ Match found! Match ID: ' + message.data.matchId);
// Auto-accept match for demo
setTimeout(() => {
ws.send(JSON.stringify({
type: 'accept_match',
data: { matchId: message.data.matchId }
}));
}, 1000);
break;
case 'match_started':
log('๐ฎ Match started! Room: ' + message.data.roomId);
break;
case 'p2p_room_joined':
currentP2PRoom = message.data.roomId;
log('๐ Joined P2P room: ' + currentP2PRoom);
break;
case 'p2p_room_left':
log('๐ช Left P2P room: ' + currentP2PRoom);
currentP2PRoom = null;
break;
}
}
function joinRoom() {
const roomId = document.getElementById('roomId').value;
ws.send(JSON.stringify({
type: 'join_room',
data: { roomId }
}));
}
function leaveRoom() {
if (currentRoom) {
ws.send(JSON.stringify({
type: 'leave_room',
data: { roomId: currentRoom }
}));
}
}
function joinQueue() {
const queueType = document.getElementById('queueType').value;
const skill = parseInt(document.getElementById('skillLevel').value);
ws.send(JSON.stringify({
type: 'join_queue',
data: {
queueType,
criteria: { skill }
}
}));
}
function leaveQueue() {
if (currentQueue) {
ws.send(JSON.stringify({
type: 'leave_queue',
data: { queueType: currentQueue }
}));
}
}
function joinP2PRoom() {
const roomId = document.getElementById('p2pRoomId').value;
ws.send(JSON.stringify({
type: 'p2p_join_room',
data: { roomId }
}));
}
function leaveP2PRoom() {
if (currentP2PRoom) {
ws.send(JSON.stringify({
type: 'p2p_leave_room',
data: { roomId: currentP2PRoom }
}));
}
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
ws.send(JSON.stringify({
type: 'custom_message',
data: { text: message }
}));
input.value = '';
}
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
// Allow Enter key to send messages
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
// Initialize
updateStatus();
log('๐ ONILib Example Client loaded');
</script>
</body>
</html>
`;
await writeFile(path.join(projectPath, 'examples', 'client.html'), clientContent);
console.log('๐ Created examples/client.html');
}
async createPackageJson(projectPath, projectName) {
const packageJsonPath = path.join(projectPath, 'package.json');
try {
await access(packageJsonPath);
console.log('๐ฆ package.json already exists, skipping...');
return;
} catch {
// File doesn't exist, create it
}
const packageContent = {
name: projectName,
version: '1.0.0',
description: 'ONILib project',
main: 'src/index.js',
scripts: {
start: 'node src/index.js',
dev: 'nodemon src/index.js',
test: 'jest',
build: 'echo "Build completed"'
},
dependencies: {
'onilib': '^1.0.0'
},
devDependencies: {
nodemon: '^3.0.1',
jest: '^29.7.0'
},
keywords: ['websocket', 'webrtc', 'realtime', 'gaming'],
author: '',
license: 'MIT'
};
await writeFile(packageJsonPath, JSON.stringify(packageContent, null, 2));
console.log('๐ฆ Created package.json');
}
async createReadme(projectPath, projectName) {
const readmeContent = `
An ONILib project for real-time applications.
- ๐ **Authentication**: JWT and API Key support
- ๐ **Real-time Communication**: WebSocket server with rooms
- ๐ฎ **Matchmaking**: Queue-based player matching
- ๐ **P2P Support**: WebRTC signaling for peer-to-peer connections
- ๐พ **Storage**: Flexible storage adapters (Memory, SQLite, PostgreSQL)
- ๐ **Admin Panel**: REST API for monitoring and management
## Quick Start
1. **Install dependencies**:
\`\`\`bash
npm install
\`\`\`
2. **Start the server**:
\`\`\`bash
npm start
# or for development with auto-reload:
npm run dev
\`\`\`
3. **Test the client**:
Open \`examples/client.html\` in your browser and connect to the WebSocket server.
## Configuration
Edit \`noi.config.js\` to customize your application settings:
- **Authentication**: Configure JWT secrets and API keys
- **WebSocket**: Set ports and connection limits
- **Storage**: Choose between memory, SQLite, or PostgreSQL
- **Matchmaking**: Configure queue settings and match criteria
- **P2P**: Set up WebRTC STUN/TURN servers
## API Endpoints
### WebSocket Messages
- \`auth\` - Authenticate with API key or JWT
- \`join_room\` - Join a chat room
- \`leave_room\` - Leave a chat room
- \`join_queue\` - Join matchmaking queue
- \`leave_queue\` - Leave matchmaking queue
- \`p2p_join_room\` - Join P2P room for WebRTC
### Admin REST API
Access the admin panel at \`http://localhost:3001\`:
- \`GET /health\` - Health check
- \`GET /api/system/stats\` - System statistics
- \`GET /api/realtime/clients\` - Connected clients
- \`GET /api/matchmaking/queues\` - Active queues
- \`GET /api/p2p/rooms\` - P2P rooms
## Development
### Running Tests
\`\`\`bash
npm test
\`\`\`
### CLI Commands
- \`onilib init\` - Initialize a new project
- \`onilib start\` - Start the server
- \`onilib test\` - Run tests
- \`onilib build\` - Build for production
## Environment Variables
- \`NODE_ENV\` - Environment (development/production)
- \`PORT\` - HTTP server port
- \`WS_PORT\` - WebSocket server port
- \`JWT_SECRET\` - JWT signing secret
- \`API_KEY\` - Default API key
- \`STORAGE_TYPE\` - Storage adapter (memory/sqlite/postgres)
- \`STORAGE_PATH\` - Database file path (for SQLite)
## License
MIT
`;
await writeFile(path.join(projectPath, 'README.md'), readmeContent);
console.log('๐ Created README.md');
}
async createGitignore(projectPath) {
const gitignoreContent = `
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pids
*.pid
*.seed
*.pid.lock
coverage/
.nyc_output
.grunt
bower_components
.lock-wscript
build/Release
node_modules/
jspm_packages/
.npm
.node_repl_history
*.tgz
.yarn-integrity
.env
.env.test
.env.production
.env.local
.cache
.parcel-cache
.next
.nuxt
.vuepress/dist
.serverless
.fusebox/
.dynamodb/
.tern-port
data/
*.db
*.sqlite
*.sqlite3
logs/
*.log
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
`;
await writeFile(path.join(projectPath, '.gitignore'), gitignoreContent);
console.log('๐ Created .gitignore');
}
async start(args) {
console.log('๐ Starting ONILib server...');
const isDev = args.includes('--dev') || args.includes('-d');
const command = isDev ? 'npm run dev' : 'npm start';
const child = spawn(command, [], {
stdio: 'inherit',
shell: true,
cwd: process.cwd()
});
child.on('error', (error) => {
console.error('โ Failed to start server:', error.message);
process.exit(1);
});
child.on('exit', (code) => {
if (code !== 0) {
console.error(`โ Server exited with code ${code}`);
process.exit(code);
}
});
}
async test(args) {
console.log('๐งช Running tests...');
const testType = args[0];
let command = 'npm test';
if (testType === 'p2p') {
const nodes = args.find(arg => arg.startsWith('--nodes'))?.split('=')[1] || '2';
const duration = args.find(arg => arg.startsWith('--duration'))?.split('=')[1] || '30s';
console.log(`๐ Running P2P test with ${nodes} nodes for ${duration}`);
command = `node test/p2p-test.js --nodes=${nodes} --duration=${duration}`;
}
const child = spawn(command, [], {
stdio: 'inherit',
shell: true,
cwd: process.cwd()
});
child.on('error', (error) => {
console.error('โ Failed to run tests:', error.message);
process.exit(1);
});
child.on('exit', (code) => {
if (code === 0) {
console.log('โ
All tests passed!');
} else {
console.error(`โ Tests failed with code ${code}`);
process.exit(code);
}
});
}
async build(args) {
console.log('๐จ Building project...');
const child = spawn('npm run build', [], {
stdio: 'inherit',
shell: true,
cwd: process.cwd()
});
child.on('error', (error) => {
console.error('โ Build failed:', error.message);
process.exit(1);
});
child.on('exit', (code) => {
if (code === 0) {
console.log('โ
Build completed successfully!');
} else {
console.error(`โ Build failed with code ${code}`);
process.exit(code);
}
});
}
help() {
console.log(`
๐ ONILib CLI (onilib)
Usage: onilib <command> [options]
Commands:
init [name] Initialize a new ONILib project
start [--dev] Start the server (--dev for development mode)
test [type] Run tests
test p2p --nodes=N --duration=Xs Run P2P stress test
build Build the project for production
help Show this help message
Examples:
onilib init my-game-server
onilib start --dev
onilib test
onilib test p2p --nodes=10 --duration=60s
onilib build
For more information, visit: https://github.com/yourusername/onilib
`);
}
}
// Run CLI if called directly
if (require.main === module) {
const cli = new NoiCLI();
cli.run();
}
module.exports = { NoiCLI };