diff --git a/.gitignore b/.gitignore index 8b5e3dcd..21ef4b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ flamegraph.html profile* package-lock.json + +test/typescript/*.js +test/typescript/*.map diff --git a/package.json b/package.json index d95da9f9..69b4c176 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,21 @@ "version": "0.33.0", "description": "Stream-based MQTT broker", "main": "aedes.js", + "types": "types/index.d.ts", "scripts": { "lint": "standard", + "tslint": "tslint types/**/*.d.ts", + "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", + "typescript-compile-execute": "node test/typescript/*.js", + "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", "test": "tape test/*.js test/*/*.js | faucet", - "ci": "npm run lint; npm run coverage", + "ci": "npm run lint && npm run typescript-test && npm run coverage", "coverage": "istanbul cover tape test/*.js test/*/*.js", "coveralls": "cat coverage/lcov.info | coveralls" }, "pre-commit": [ "lint", + "tslint", "test" ], "repository": { @@ -32,6 +38,7 @@ "author": "Matteo Collina ", "license": "MIT", "devDependencies": { + "@types/node": "^8.10.0", "compute-mode": "^1.0.0", "concat-stream": "^1.4.7", "convert-hrtime": "^2.0.0", @@ -44,6 +51,9 @@ "pre-commit": "^1.0.10", "standard": "^10.0.3", "tape": "^4.8.0", + "tslint": "^5.10.0", + "tslint-config-standard": "^7.0.0", + "typescript": "^2.8.3", "websocket-stream": "^5.1.1" }, "dependencies": { @@ -56,7 +66,7 @@ "fastseries": "^1.5.0", "from2": "^2.1.0", "mqemitter": "^2.2.0", - "mqtt-packet": "^5.4.0", + "mqtt-packet": "^5.6.0", "pump": "^2.0.1", "retimer": "^1.1.0", "reusify": "^1.0.3", diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json new file mode 100644 index 00000000..0b8d393a --- /dev/null +++ b/test/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "alwaysStrict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "sourceMap": true + } +} diff --git a/test/typescript/typings.ts b/test/typescript/typings.ts new file mode 100644 index 00000000..ebb88000 --- /dev/null +++ b/test/typescript/typings.ts @@ -0,0 +1,98 @@ +// relative path uses package.json {"types":"types/index.d.ts", ...} + +import Aedes = require ('../..') +import { IPublishPacket, ISubscribePacket, ISubscription, IUnsubscribePacket } from 'mqtt-packet' +import { createServer } from 'net' + +const aedes = Aedes({ + concurrency: 100, + heartbeatInterval: 60000, + connectTimeout: 30000, + authenticate: (client, username: string, password: string, callback) => { + if (username === 'test' && password === 'test') { + callback(null, true) + } else { + const error = new Error() as Error & { returnCode: number } + error.returnCode = 1 + + callback(error, false) + } + }, + authorizePublish: (client, packet: IPublishPacket, callback) => { + if (packet.topic === 'aaaa') { + return callback(new Error('wrong topic')) + } + + if (packet.topic === 'bbb') { + packet.payload = new Buffer('overwrite packet payload') + } + + callback(null) + }, + authorizeSubscribe: (client, sub: ISubscription, callback) => { + if (sub.topic === 'aaaa') { + return callback(new Error('wrong topic')) + } + + if (sub.topic === 'bbb') { + // overwrites subscription + sub.qos = 2 + } + + callback(null, sub) + }, + authorizeForward: (client, packet: IPublishPacket) => { + if (packet.topic === 'aaaa' && client.id === 'I should not see this') { + return null + // also works with return undefined + } else if (packet.topic === 'aaaa' && client.id === 'I should not see this either') { + return + } + + if (packet.topic === 'bbb') { + packet.payload = new Buffer('overwrite packet payload') + } + + return packet + } +}) + +const server = createServer(aedes.handle) + +aedes.on('client', client => { + console.log(`client: ${client.id} connected`) +}) + +aedes.on('clientDisconnect', client => { + console.log(`client: ${client.id} disconnected`) +}) + +aedes.on('keepaliveTimeout', client => { + console.log(`client: ${client.id} timed out`) +}) + +aedes.on('clientError', client => { + console.log(`client: ${client.id} error`) +}) + +aedes.on('connectionError', client => { + console.log('connectionError') +}) + +aedes.subscribe('aaaa', (packet: ISubscribePacket, cb) => { + console.log('cmd') + console.log(packet.subscriptions) + cb() +}, () => { + console.log('done subscribing') +}) + +aedes.unsubscribe('aaaa', (packet: IUnsubscribePacket, cb) => { + console.log('cmd') + console.log(packet.unsubscriptions) + cb() +}, () => { + console.log('done unsubscribing') +}) + +aedes.close() diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..88877985 --- /dev/null +++ b/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "tslint-config-standard" +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..237ed5f1 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,82 @@ +/// + +import { IPublishPacket, ISubscribePacket, ISubscription, IUnsubscribePacket } from 'mqtt-packet' +import { Duplex } from 'stream' +import EventEmitter = NodeJS.EventEmitter + +declare enum AuthErrorCode { + UNNACCEPTABLE_PROTOCOL = 1, + IDENTIFIER_REJECTED = 2, + SERVER_UNAVAILABLE = 3, + BAD_USERNAME_OR_PASSWORD = 4 +} + +interface Client extends EventEmitter { + id: string + clean: boolean + + on (event: 'error', cb: (err: Error) => void): this + + publish (message: IPublishPacket, callback?: () => void): void + subscribe ( + subscriptions: ISubscription | ISubscription[] | ISubscribePacket, + callback?: () => void + ): void + unsubscribe (topicObjects: ISubscription | ISubscription[], callback?: () => void): void + close (callback?: () => void): void +} + +type AuthenticateCallback = ( + client: Client, + username: string, + password: string, + done: (err: Error & { returnCode: AuthErrorCode } | null, success: boolean | null) => void +) => void + +type AuthorizePublishCallback = (client: Client, packet: IPublishPacket, done: (err?: Error | null) => void) => void + +type AuthorizeSubscribeCallback = (client: Client, subscription: ISubscription, done: (err: Error | null, subscription?: ISubscription | null) => void) => void + +type AuthorizeForwardCallback = (client: Client, packet: IPublishPacket) => IPublishPacket | null | void + +type PublishedCallback = (packet: IPublishPacket, client: Client, done: () => void) => void + +interface AedesOptions { + mq?: any + persistence?: any + concurrency?: number + heartbeatInterval?: number + connectTimeout?: number + authenticate?: AuthenticateCallback + authorizePublish?: AuthorizePublishCallback + authorizeSubscribe?: AuthorizeSubscribeCallback + authorizeForward?: AuthorizeForwardCallback + published?: PublishedCallback +} + +interface Aedes extends EventEmitter { + handle: (stream: Duplex) => void + + authenticate: AuthenticateCallback + authorizePublish: AuthorizePublishCallback + authorizeSubscribe: AuthorizeSubscribeCallback + authorizeForward: AuthorizeForwardCallback + published: PublishedCallback + + on (event: 'client' | 'clientDisconnect' | 'keepaliveTimeout', cb: (client: Client) => void): this + on (event: 'clientError' | 'connectionError', cb: (client: Client, error: Error) => void): this + on (event: 'ping', cb: (packet: any, client: Client) => void): this + + publish (packet: IPublishPacket & { topic: string | Buffer }, done: () => void): void + subscribe (topic: string, callback: (packet: ISubscribePacket, cb: () => void) => void, done: () => void): void + unsubscribe ( + topic: string, + callback: (packet: IUnsubscribePacket, cb: () => void) => void, + done: () => void + ): void + close (callback?: () => void): void +} + +declare function aedes (options?: AedesOptions): Aedes + +export = aedes