Skip to content

Commit

Permalink
version 5. refactoring and changed the middleware interface
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyguerra committed Oct 23, 2022
1 parent 144bdd8 commit 8df7531
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 232 deletions.
27 changes: 0 additions & 27 deletions .travis.yml

This file was deleted.

5 changes: 0 additions & 5 deletions WHERE-I-LEFT-OFF.md

This file was deleted.

119 changes: 119 additions & 0 deletions examples/NewDesign.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import EventEmitter from 'node:events'

class InMemoryCommandQueue {
#queue = []
constructor(){}
async enqueue(command){
this.#queue.push(command)
}
*dequeue(){
yield this.#queue.shift()
}
}

class Hubot extends EventEmitter {
#queue
#timer
constructor(state = {}){
super()
this.#queue = new InMemoryCommandQueue()
this.events = []
this.decisionState = state
this.listeners = new Set()
}
iterateOverQueue(){
for(let request of this.#queue.dequeue()){
if(!request) continue
try{
let event = this.#handle(this.decisionState, request)
if(event) this.events.push(event)
}catch(e){
console.error(e)
throw e
}
}
}
#handle(state, request){
request.connection.write(request.message)
for(let listener of this.listeners){
listener.receive(request)
}
return request
}
start(period = 1000){
this.#timer = setInterval(this.iterateOverQueue.bind(this), period)
}
stop(){
clearInterval(this.#timer)
}
recevieMessage(message){
this.emit('incoming message', message)
}
addIncomingRequest(request){
this.#queue.enqueue(request)
}
}

class Chat extends EventEmitter{
#url
#protocols
#buffer = []
constructor(url, protocols){
super()
this.#url = url
this.#protocols = protocols
}
addEventListener(eventName, listener){
this.addListener(eventName, listener)
}
removeEventListener(eventName, listener){
this.removeListener(eventName, listener)
}
sendMessage(message){
this.emit('message', this, message)
}
write(data){
console.log('writing data', data)
this.#buffer.push(data)
}
*read(){
yield this.#buffer.shift()
}

}
class IncomingRequest {
constructor(connection, message){
this.connection = connection
this.message = message
}
}
await test('Explore simplifying the software design of Hubot', async (t)=>{
await t.test('Decouple receiving a message from responding to a message', async ()=> {
const hubot = new Hubot()
hubot.listeners.add({
receive(request){
if(!/yo/.test(request.message)) return
console.log('listening to ', request.message)
}
})
hubot.start()
const chat = new Chat('ws://localhost/testing')
chat.addEventListener('message', (connection, message)=>{
hubot.addIncomingRequest(new IncomingRequest(connection, message))
})
chat.sendMessage('Hi')
let data = null
await new Promise((resolve, reject)=>{
let timer = setInterval(()=>{
data = chat.read().next().value
if(!data) return
clearInterval(timer)
hubot.stop()
assert.deepEqual(data, 'Hi')
resolve()
}, 100)
})
})
})
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hubot",
"version": "4.0.3",
"version": "5.0.0",
"author": "hubot",
"keywords": [
"github",
Expand All @@ -25,8 +25,8 @@
"nodemon": "github:joeyguerra/nodemon"
},
"engines": {
"node": "> 18.0.0",
"npm": "> 8.0.0"
"node": "> 19.0.0",
"npm": "> 8.19.0"
},
"main": "./index.mjs",
"bin": {
Expand All @@ -36,6 +36,6 @@
"start": "node bin/hubot.mjs",
"start:local": "node bin/hubot.mjs",
"test": "node --test",
"test:local": "nodemon --test"
"test:local": "nodemon"
}
}
10 changes: 5 additions & 5 deletions src/adapter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Adapter extends EventEmitter {
// envelope - A Object with message, room and user details.
// strings - One or more Strings for each message to send.
//
// Returns nothing.
// Returns responses from the chat source.
async send (envelope, ...strings) {}

// Public: Raw method for sending emote data back to the chat source.
Expand All @@ -25,7 +25,7 @@ class Adapter extends EventEmitter {
// envelope - A Object with message, room and user details.
// strings - One or more Strings for each message to send.
//
// Returns nothing.
// Returns responses from the chat source.
async emote (envelope, ...strings) {
return await this.send(envelope, ...strings)
}
Expand All @@ -36,23 +36,23 @@ class Adapter extends EventEmitter {
// envelope - A Object with message, room and user details.
// strings - One or more Strings for each reply to send.
//
// Returns nothing.
// Returns responses from the chat source.
async reply (envelope, ...strings) {}

// Public: Raw method for setting a topic on the chat source. Extend this.
//
// envelope - A Object with message, room and user details.
// strings - One more more Strings to set as the topic.
//
// Returns nothing.
// Returns responses from the chat source.
async topic (envelope, ...strings) {}

// Public: Raw method for playing a sound in the chat source. Extend this.
//
// envelope - A Object with message, room and user details.
// strings - One or more strings for each play message to send.
//
// Returns nothing
// Returns responses from the chat source.
async play (envelope, ...strings) {}

// Public: Raw method for invoking the bot to run. Extend this.
Expand Down
17 changes: 9 additions & 8 deletions src/listener.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use strict'

import crypto from 'node:crypto'
import {inspect} from 'util'
import Robot from './robot.mjs'
import {TextMessage} from './message.mjs'
import Middleware from './middleware.mjs'
import Response from './response.mjs'

export class Listener {
// Listeners receive every message from the chat source and decide if they
// want to act on it.
Expand All @@ -29,9 +31,9 @@ export class Listener {
this.callback = this.options
this.options = {}
}
// TODO: What?
if (this.options.id == null) {
this.options.id = null

if (!this.options?.id) {
this.options.id = crypto.webcrypto.randomUUID()
}

if (this.callback == null || typeof this.callback !== 'function') {
Expand All @@ -49,8 +51,7 @@ export class Listener {
// middleware - Optional Middleware object to execute before the Listener callback
// callback - Optional Function called with a boolean of whether the matcher matched
//
// Returns a boolean of whether the matcher matched.
// Returns before executing callback
// Returns an instance of a Response.
async call (message, middleware, didMatchCallback) {
// middleware argument is optional
if (!didMatchCallback && typeof middleware === 'function') {
Expand All @@ -77,9 +78,9 @@ export class Listener {
const response = new Response(this.robot, message, match)
let shouldExecuteCallback = true
try{
await middleware.execute({ listener: this, response })
await middleware.execute(response)
}catch(err){
this.robot.emit('error', err, response)
this.robot.emit(Robot.EVENTS.ERROR, err, response)
shouldExecuteCallback = false
}
shouldExecuteCallback = shouldExecuteCallback && !response.message.done
Expand Down
29 changes: 16 additions & 13 deletions src/middleware.mjs
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
'use strict'

import Robot from './robot.mjs'
class Middleware {
constructor (robot) {
this.robot = robot
this.stack = []
this.stack = new Set()
}

// Public: Execute all middleware with await/async
//
// context - context object that is passed through the middleware stack.
// When handling errors, this is assumed to have a `response` property.
//
// Returns nothing
// Returns before executing any middleware
async execute (context) {
// response - Response object that is passed through the middleware stack.
// Returns response
async execute (response) {
for await (let middleware of this.stack) {
const shouldContinue = await middleware(this.robot, context)
if(!shouldContinue) break
let shouldContinue = true
try{
shouldContinue = await middleware(this.robot, response)
}catch(e){
shouldContinue = false
this.robot.emit(Robot.EVENTS.ERROR, e, response)
}finally{
if(!shouldContinue) break
}
}
return context
return response
}

// Public: Registers new middleware
Expand All @@ -27,7 +30,7 @@ class Middleware {
//
// Returns nothing.
register (middleware) {
this.stack.push(middleware)
this.stack.add(middleware)
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/response.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Response {
// strings - One or more strings to be posted. The order of these strings
// should be kept intact.
//
// Returns nothing.
// Returns the chat sources response if has one.
async send (...strings) {
let options = {
plaintext: strings.some(s => !(typeof s != 'string'))
Expand All @@ -37,7 +37,7 @@ class Response {
// strings - One or more strings to be posted. The order of these strings
// should be kept intact.
//
// Returns nothing.
// Returns the chat sources response if has one.
async emote (...strings) {
let options = {
plaintext: strings.some(s => !(typeof s != 'string'))
Expand All @@ -50,7 +50,7 @@ class Response {
// strings - One or more strings to be posted. The order of these strings
// should be kept intact.
//
// Returns nothing.
// Returns the chat sources response if has one.
async reply (...strings) {
let options = {
plaintext: strings.some(s => !(typeof s != 'string'))
Expand All @@ -63,7 +63,7 @@ class Response {
// strings - One or more strings to set as the topic of the
// room the bot is in.
//
// Returns nothing.
// Returns the chat sources response if has one.
async topic (...strings) {
let options = {
plaintext: strings.some(s => !(typeof s != 'string'))
Expand All @@ -76,7 +76,7 @@ class Response {
// strings - One or more strings to be posted as sounds to play. The order of
// these strings should be kept intact.
//
// Returns nothing
// Returns the chat sources response if has one.
async play (...strings) {
return await this.runWithMiddleware('play', {}, ...strings)
}
Expand All @@ -86,7 +86,7 @@ class Response {
// strings - One or more strings to be posted. The order of these strings
// should be kept intact.
//
// Returns nothing
// Returns the chat sources response if has one.
async locked (...strings) {
let options = {
plaintext: strings.some(s => !(typeof s != 'string'))
Expand Down
Loading

0 comments on commit 8df7531

Please sign in to comment.