Skip to content

Commit

Permalink
feat: replace express server with native http server
Browse files Browse the repository at this point in the history
  • Loading branch information
fletcherist committed Jul 30, 2018
1 parent 03a7e20 commit 2cad4f2
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 107 deletions.
1 change: 0 additions & 1 deletion .eslintignore

This file was deleted.

2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"singleQuote": true,
"tabWidth": 4,
"semi": false,
"trailingComma": "es5",
"trailingComma": "all",
"arrowParens": "avoid"
}
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@
"skills",
"alice-sdk",
"alice-dialogs",
"yandex-dialogs-sdk",
"imperator"
"yandex-dialogs-sdk"
],
"files": [
"dist"
Expand All @@ -51,15 +50,14 @@
"dependencies": {
"bunyan": "^1.8.12",
"chalk": "^2.4.1",
"express": "^4.16.3",
"fuse.js": "^3.2.0",
"loglevel": "^1.6.1",
"loglevel-plugin-prefix": "^0.8.4",
"node-fetch": "^2.1.2",
"ramda": "^0.25.0"
},
"devDependencies": {
"@types/express": "^4.16.0",
"@types/node-fetch": "^2.1.2",
"@types/jest": "^23.1.3",
"@types/node": "^10.5.1",
"conventional-changelog-cli": "^2.0.0",
Expand Down
156 changes: 64 additions & 92 deletions src/alice.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import express from 'express'
import http from 'http'
import fetch from 'node-fetch'
import Commands from './commands'
import { Sessions } from './sessions'

import Scene from './scene'
import Context from './context'
import ImagesApi from './imagesApi'
import fetch from 'node-fetch'

import { selectCommand, selectSessionId, isFunction, delay, rejectsIn } from './utils'

import { selectSessionId, isFunction, rejectsIn } from './utils'
import { applyMiddlewares } from './middlewares'

import stateMiddleware from './middlewares/stateMiddleware'
import eventEmitter from './eventEmitter'

import { IConfig, IAlice } from './types/alice'
import { ICommand } from './types/command'
import { IContext } from './types/context'
import { WebhookResponse, WebhookRequest } from './types/webhook'
import { EventInterface, EventEmitterInterface } from './types/eventEmitter'
import eventEmitter from './eventEmitter'

import {
EVENT_MESSAGE_RECIEVED,
EVENT_MESSAGE_NOT_SENT,
DEFAULT_TIMEOUT_CALLBACK_MESSAGE,
EVENT_MESSAGE_PROXIED,
EVENT_MESSAGE_PROXY_ERROR,
EVENT_SERVER_STARTED,
EVENT_SERVER_STOPPED,
} from './constants'

const DEFAULT_SESSIONS_LIMIT: number = 1000
Expand All @@ -35,9 +35,10 @@ export default class Alice implements IAlice {
public scenes: Scene[]

protected anyCallback: (ctx: IContext) => void
protected config: IConfig
protected commands: Commands
private welcomeCallback: (ctx: IContext) => void
private timeoutCallback: (ctx: IContext) => void
protected commands: Commands
private middlewares: any[]
private currentScene: Scene | null
private sessions: Sessions
Expand All @@ -46,7 +47,6 @@ export default class Alice implements IAlice {
close: () => void
}
private eventEmitter: EventEmitterInterface
protected config: IConfig

constructor(config: IConfig = {}) {
this.anyCallback = null
Expand Down Expand Up @@ -83,27 +83,16 @@ export default class Alice implements IAlice {
this.middlewares.push(middleware)
}

/**
* Set up the command
* @param {string | Array<string> | regex} name — Trigger for the command
* @param {Function} callback — Handler for the command
*/
public command(name: ICommand, callback: (IContext) => void) {
this.commands.add(name, callback)
}

/**
* Стартовая команда на начало сессии
*/
// Handler for every new session
public welcome(callback: (IContext) => void): void {
this.welcomeCallback = callback
}

/**
* Если среди команд не нашлось той,
* которую запросил пользователь,
* вызывается этот колбек
*/
public command(name: ICommand, callback: (IContext) => void) {
this.commands.add(name, callback)
}

// If no matches, this fn will be invoked
public any(callback: (IContext) => void): void {
this.anyCallback = callback
}
Expand Down Expand Up @@ -169,25 +158,23 @@ export default class Alice implements IAlice {
req,
sendResponse,
context,
'leave'
'leave',
)
session.setData('currentScene', null)
return sceneResponse
} else {
const sceneResponse = await matchedScene.handleSceneRequest(
req,
sendResponse,
context
context,
)
if (sceneResponse) {
return sceneResponse
}
}
}
} else {
/**
* Looking for scene's activational phrases
*/
// Looking for scene's activational phrases
let matchedScene = null
for (const scene of this.scenes) {
const result = await scene.isEnterCommand(context)
Expand All @@ -202,7 +189,7 @@ export default class Alice implements IAlice {
req,
sendResponse,
context,
'enter'
'enter',
)
if (sceneResponse) {
return sceneResponse
Expand All @@ -211,48 +198,35 @@ export default class Alice implements IAlice {
}

const requestedCommands = await this.commands.search(context)
/**
* Если новая сессия, то запускаем стартовую команду
*/
if (req.session.new && this.welcomeCallback) {
/**
* Patch context with middlewares
*/
if (this.welcomeCallback) {
return await this.welcomeCallback(context)
}
}
/**
* Команда нашлась в списке.
* Запускаем её обработчик.
*/

// It's a match with registered command
if (requestedCommands.length !== 0) {
const requestedCommand: ICommand = requestedCommands[0]
context.command = requestedCommand
return await requestedCommand.callback(context)
}

/**
* Такой команды не было зарегестрировано.
* Переходим в обработчик исключений
*/
// No matches with commands
if (!this.anyCallback) {
throw new Error(
[
`alice.any(ctx => ctx.reply('404')) Method must be defined`,
'to catch anything that not matches with commands',
].join('\n')
].join('\n'),
)
}
return await this.anyCallback(context)
}

/**
* Same as handleRequestBody, but syntax shorter
*/
// same as handleRequestBody, syntax sugar
public async handleRequest(
req: WebhookRequest,
sendResponse?: (res: WebhookResponse) => void
sendResponse?: (res: WebhookResponse) => void,
): Promise<any> {
return await Promise.race([
/* proxy request to dev server, if enabled */
Expand All @@ -267,49 +241,46 @@ export default class Alice implements IAlice {
this.timeoutCallback(new Context({ req, sendResponse }))
})
}
/**
* Метод создаёт сервер, который слушает указанный порт.
* Когда на указанный URL приходит POST запрос, управление
* передаётся в @handleRequestBody
*
* При получении ответа от @handleRequestBody, результат
* отправляется обратно.
*/

public async listen(webhookPath = '/', port = 80, callback?: () => void) {
return new Promise(resolve => {
const app = express()
app.use(express.json())
app.post(webhookPath, async (req, res) => {
if (this.config.oAuthToken) {
res.setHeader('Authorization', this.config.oAuthToken)
}
res.setHeader('Content-type', 'application/json')

let responseAlreadySent = false
const handleResponseCallback = (response: WebhookResponse) => {
/* dont answer twice */
if (responseAlreadySent) {
return false
this.server = http
.createServer(async (request, response) => {
const body = []
request
.on('data', chunk => {
body.push(chunk)
})
.on('end', async () => {
const requestData = Buffer.concat(body).toString()
if (request.method === 'POST' && request.url === webhookPath) {
const handleResponseCallback = (responseBody: WebhookResponse) => {
response.statusCode = 200
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify(responseBody))
}
try {
const requestBody = JSON.parse(requestData)
return await this.handleRequest(
requestBody,
handleResponseCallback,
)
} catch (error) {
throw new Error(error)
}
} else {
response.statusCode = 400
response.end()
}
})
})
.listen(port, () => {
eventEmitter.dispatch(EVENT_SERVER_STARTED)
if (isFunction(callback)) {
return callback()
}
res.send(response)
responseAlreadySent = true
}
try {
return await this.handleRequest(req.body, handleResponseCallback)
} catch (error) {
throw new Error(error)
}
})
this.server = app.listen(port, () => {
// Resolves with callback function
if (isFunction(callback)) {
return callback.call(this)
}

// If no callback specified, resolves as a promise.
return resolve()
// Resolves with promise if no callback set
})
return resolve()
})
})
}

Expand All @@ -330,16 +301,17 @@ export default class Alice implements IAlice {
return await this.imagesApi.getImages()
}

public stopListening() {
public stopListening(): void {
if (this.server && this.server.close) {
this.server.close()
eventEmitter.dispatch(EVENT_SERVER_STOPPED)
}
}

private async handleProxyRequest(
request: WebhookRequest,
devServerUrl: string,
sendResponse?: (res: WebhookResponse) => void
sendResponse?: (res: WebhookResponse) => void,
) {
try {
const res = await fetch(devServerUrl, {
Expand Down
2 changes: 1 addition & 1 deletion src/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const button = (params: IButton | string): IButton => {
}

return {
title: title,
title,
tts,
url,
hide,
Expand Down
2 changes: 1 addition & 1 deletion src/buttonBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IButton } from './types/button'

export default class ButtonBuilder {
button: IButton
private button: IButton
constructor(buttonConstructor?: IButton) {
/* No button object passed to the constructor */
if (!buttonConstructor) {
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const EVENT_MESSAGE_NOT_SENT = 'messageNotSent'
export const EVENT_MESSAGE_PROXIED = 'messageProxied'
export const EVENT_MESSAGE_PROXY_ERROR = 'messageProxyError'

export const EVENT_SERVER_STARTED = 'serverStarted'
export const EVENT_SERVER_STOPPED = 'serverStopped'

export const DEFAULT_TIMEOUT_CALLBACK_MESSAGE =
'Извините, но я не успела найти ответ за отведенное время.'
export const EMPTY_SYMBOL = 'ᅠ '
Expand Down
8 changes: 4 additions & 4 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,17 @@ export default class Context implements IContext {
text: EMPTY_SYMBOL,
card: compose(
bigImageCard,
image
image,
)(params),
})
}),
)
return this._sendReply(message)
}
const message = this._createReply(
reply({
text: EMPTY_SYMBOL,
card: bigImageCard(params),
})
}),
)
return this._sendReply(message)
}
Expand All @@ -123,7 +123,7 @@ export default class Context implements IContext {
reply({
text: EMPTY_SYMBOL,
card: itemsListCard(params),
})
}),
)
return this._sendReply(message)
}
Expand Down
Loading

0 comments on commit 2cad4f2

Please sign in to comment.