const isUndefined = require('lodash/isUndefined');
const isString = require('lodash/isString');
const isObject = require('lodash/isObject');
const isFunction = require('lodash/isFunction');
const values = require('lodash/values');
const assign = require('lodash/assign');
const {generateDummyId} = require('./utils');
const EventEmitterExtra = require('event-emitter-extra');
const LineError = require('./error');
/**
* Message class.
*
* @private
* @class Message
* @extends {EventEmitterExtra}
*/
class Message extends EventEmitterExtra {
static parse(raw) {
try {
const data = JSON.parse(raw);
// If error is error-like object, construct real error
if (isObject(data.e) && isString(data.e.name) && isString(data.e.message)) {
data.e = assign(new Error(), data.e);
}
return new Message({
name: data.n,
payload: data.p,
err: data.e,
id: data.i
});
} catch(err) {
throw new LineError(Message.ErrorCode.INVALID_JSON, `Could not parse incoming message.`);
}
}
constructor({name, payload, id, err}) {
super();
try {
JSON.stringify(payload);
JSON.stringify(err);
} catch (err) {
throw new LineError(
Message.ErrorCode.INVALID_JSON,
`Message payload or error must be json-friendly. Maybe circular json?`
);
}
this.name = name;
this.payload = payload;
this.id = id;
this.err = err;
this.isResponded_ = false;
}
setId(id = generateDummyId()) {
this.id = id;
return id;
}
createResponse(err, payload) {
return new Message({name: '_r', payload, err, id: this.id});
}
/**
* Resolves the message with sending a response back. If the source
* does not expecting a response, you don't need to call these methods.
*
* This method can throw:
* - `Message.ErrorCode.MISSING_ID`: Message source is not expecting a response.
* - `Message.ErrorCode.ALREADY_RESPONDED`: This message is already responded.
* - `Message.ErrorCode.INVALID_JSON`: Could not stringify payload. Probably circular json.
*
* @param {any=} payload
*/
resolve(payload) {
if (isUndefined(this.id)) {
throw new LineError(Message.ErrorCode.MISSING_ID, `This message could not be resolved (no id)`);
}
if (this.isResponded_) {
throw new LineError(Message.ErrorCode.ALREADY_RESPONDED, `This message has already responded`);
}
try {
JSON.stringify(payload);
} catch (err_) {
throw new LineError(
Message.ErrorCode.INVALID_JSON,
`Message must be resolved with json-friendly payload. Maybe circular json?`
);
}
this.isResponded_ = true;
this.emit('resolved', payload);
}
/**
* Rejects the message, with sending error response back to the source.
*
* This method can throw:
* - `Message.ErrorCode.MISSING_ID`: Message source is not expecting a response.
* - `Message.ErrorCode.ALREADY_RESPONDED`: This message is already responded.
* - `Message.ErrorCode.INVALID_JSON`: Could not stringify payload. Probably circular json.
*
* @param {any=} err
*/
reject(err) {
if (isUndefined(this.id)) {
throw new LineError(Message.ErrorCode.MISSING_ID, `This message could not be rejected (no id)`);
}
if (this.isResponded_) {
throw new LineError(Message.ErrorCode.ALREADY_RESPONDED, `This message has already responded`);
}
try {
JSON.stringify(err);
} catch (err_) {
throw new LineError(
Message.ErrorCode.INVALID_JSON,
`Message must be resolved with json-friendly payload. Maybe circular json?`
);
}
this.isResponded_ = true;
this.emit('rejected', err);
}
toString() {
const data = {n: this.name};
if (!isUndefined(this.payload))
data.p = this.payload;
if (!isUndefined(this.id))
data.i = this.id;
if (!isUndefined(this.err)) {
data.e = this.err instanceof Error ? assign({
name: this.err.name,
message: this.err.message
}, this.err) : this.err;
}
// We're sure the data is json-friendly
return JSON.stringify(data);
}
dispose() {
const events = this.eventNames();
events.forEach(event => this.removeAllListeners(event));
}
}
/**
* These message names are reserved for internal usage.
* We recommend to not use any message name starts with `_` (underscore).
*
* @static
* @readonly
* @enum {string}
*/
Message.Name = {
/**
* `_r`
*/
RESPONSE: '_r',
/**
* `_h`
*/
HANDSHAKE: '_h',
/**
* `_p`
*/
PING: '_p'
};
Message.ReservedNames = values(Message.Name);
/**
* @static
* @readonly
* @enum {string}
*/
Message.ErrorCode = {
/**
* `mInvalidJson`
*/
INVALID_JSON: 'mInvalidJson',
/**
* `mMissingId`
*/
MISSING_ID: 'mMissingId',
/**
* `mAlreadyResponded`
*/
ALREADY_RESPONDED: 'mAlreadyResponded',
};
module.exports = Message;