Skip to content

Commit

Permalink
feat: Implement protocol
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
elementbound committed Jun 3, 2023
1 parent 41aaf31 commit 525952b
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 3 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@foxssake/noray",
"version": "0.13.6",
"version": "0.14.0",
"description": "Online multiplayer orchestrator and potential game platform",
"main": "src/noray.mjs",
"bin": {
Expand Down
13 changes: 13 additions & 0 deletions src/echo/echo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Noray } from '../noray.mjs'
import logger from '../logger.mjs'

const log = logger.child({ name: 'Echo' })

Noray.hook(noray => {
log.info('Adding echo command')

noray.protocolServer.on('echo', (data, socket) => {
socket.write(`echo ${data}\n`)
log.info('Echoing: %s', data)
})
})
18 changes: 16 additions & 2 deletions src/noray.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import * as net from 'node:net'
import { EventEmitter } from 'node:events'
import logger from './logger.mjs'
import { config } from './config.mjs'
import { ProtocolServer } from './protocol/protocol.server.mjs'

const defaultModules = [
'relay/relay.mjs'
'relay/relay.mjs',
'echo/echo.mjs'
]

const hooks = []
Expand All @@ -13,6 +15,9 @@ export class Noray extends EventEmitter {
/** @type {net.Server} */
#socket

/** @type {ProtocolServer} */
#protocolServer

#log = logger

/**
Expand All @@ -31,6 +36,7 @@ export class Noray extends EventEmitter {
const socket = net.createServer()

this.#socket = socket
this.#protocolServer = new ProtocolServer()

// Import modules for hooks
for (const m of modules) {
Expand All @@ -49,6 +55,11 @@ export class Noray extends EventEmitter {
config.socket.host, config.socket.port
)

socket.on('connection', conn => {
this.#protocolServer.attach(conn)
conn.on('close', () => this.#protocolServer.detach(conn))
})

this.emit('listening', config.socket.port, config.socket.host)
})
}
Expand All @@ -57,7 +68,10 @@ export class Noray extends EventEmitter {
this.#log.info('Shutting down')

this.emit('close')

this.#socket.close()
}

get protocolServer () {
return this.#protocolServer
}
}
59 changes: 59 additions & 0 deletions src/protocol/protocol.server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable */
import * as net from 'node:net'
/* eslint-enable */
import * as readline from 'node:readline'
import * as events from 'node:events'

/**
* Protocol implementation.
*
* The "protocol" itself is as follows:
*
* ```
* <command> <data>\n
* <command>\n
* ```
*
* If the incoming data fits either of the above formats, an event with the
* command's name is emitted. The data can be an arbitrary string. The same
* applies to the command, with the exception that it can't contain spaces.
*/
export class ProtocolServer extends events.EventEmitter {
#readers = new Map()

/**
* Attach socket to server.
* @param {net.Socket} socket
*/
attach (socket) {
const rl = readline.createInterface({
input: socket
})

rl.on('line', line => this.#handleLine(socket, line))
this.#readers.set(socket, rl)
}

/**
* Detach socket from server.
* @param {net.Socket} socket
*/
detach (socket) {
this.#readers.get(socket)?.close()
this.#readers.delete(socket)
}

/**
* @param {net.Socket} socket
* @param {string} line
*/
#handleLine (socket, line) {
const idx = line.indexOf(' ')

const [command, data] = idx >= 0
? [line.slice(0, idx), line.slice(idx + 1)]
: [line, '']

this.emit(command, data, socket)
}
}
72 changes: 72 additions & 0 deletions test/spec/protocol/protocol.server.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert'
import sinon from 'sinon'
import * as net from 'node:net'
import { ProtocolServer } from '../../../src/protocol/protocol.server.mjs'
import { promiseEvent, sleep } from '../../../src/utils.mjs'

describe('ProtocolServer', () => {
/** @type {net.Socket} */
let socket

/** @type {ProtocolServer} */
let server

/** @type {net.Server} */
let host

beforeEach(async () => {
server = new ProtocolServer()

host = net.createServer(conn => server.attach(conn))
host.listen()
await promiseEvent(host, 'listening')

socket = net.createConnection(host.address().port)
await promiseEvent(socket, 'connect')
})

it('should emit event with data', async () => {
// Given
const handler = sinon.mock()
server.on('command', handler)

// When
socket.write('command data\n')
await sleep(0.05)

// Then
assert.equal(handler.args[0][0], 'data')
})

it('should emit event without data', async () => {
// Given
const handler = sinon.mock()
server.on('command', handler)

// When
socket.write('command\n')
await sleep(0.05)

// Then
assert.equal(handler.args[0][0], '')
})

it('should not emit without nl', async () => {
// Given
const handler = sinon.mock()
server.on('command', handler)

// When
socket.write('command')
await sleep(0.05)

// Then
assert(handler.notCalled)
})

afterEach(() => {
socket.destroy()
host.close()
})
})

0 comments on commit 525952b

Please sign in to comment.