amplify-appsync-simulator
Version:
An AppSync Simulator to test AppSync API.
360 lines • 12.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const uuid_1 = require("uuid");
const retimer_1 = __importDefault(require("retimer"));
const steed_1 = __importDefault(require("steed"));
class Client {
constructor(connection, server) {
this.connection = connection;
this.server = server;
this.subscriptions = {};
this.nextId = 1;
this.inflight = {};
this.inflightCounter = 0;
this.keepAlive = 0;
this._lastDedupId = -1;
this._closed = false;
this._closing = false;
this.logger = server.logger;
this.setup();
}
setup() {
const connection = this.connection;
connection.on('error', () => { });
const completeConnection = () => {
this.setupTimer();
connection.connack({
returnCode: 0,
sessionPresent: false,
});
this.logger.info('client connected');
this.server.emit('clientConnected', this);
connection.on('puback', packet => {
this.setupTimer();
this.handlePubAck(packet);
});
connection.on('pingreq', () => {
this.logger.debug('pingreq');
this.setupTimer();
this.handlePingReq();
this.connection.pingresp();
});
connection.on('subscribe', packet => {
this.setupTimer();
this.handleSubscribe(packet);
});
connection.on('publish', packet => {
this.setupTimer();
this.handlePublish(packet);
});
connection.on('unsubscribe', packet => {
this.setupTimer();
this.logger.info({ packet }, 'unsubscribe received');
(0, steed_1.default)().map(this, packet.unsubscriptions, this.unsubscribeMapTo, err => {
if (err) {
this.logger.warn(err);
this.close(null, err.message);
return;
}
connection.unsuback({
messageId: packet.messageId,
});
});
});
connection.on('disconnect', () => {
this.logger.debug('disconnect requested');
this.close(null, 'disconnect request');
});
connection.on('error', this.handleError.bind(this));
connection.removeListener('error', () => { });
connection.on('close', () => {
this.onNonDisconnectClose('close');
});
};
connection.once('connect', packet => {
this.handleConnect(packet, completeConnection);
});
}
handleError(err) {
this.server.emit('clientError', err, this);
this.onNonDisconnectClose(err.message);
}
setupTimer() {
if (this.keepAlive <= 0) {
return;
}
const timeout = (this.keepAlive * 1000 * 3) / 2;
this.logger.debug({ timeout: timeout }, 'setting keepAlive timeout');
if (this.timer) {
this.timer.reschedule(timeout);
}
else {
this.timer = (0, retimer_1.default)(() => {
this.logger.info('keepAlive timeout');
this.onNonDisconnectClose('keepAlive timeout');
}, timeout);
}
}
doForward(err, packet) {
if (err) {
return this.connection && this.connection.emit('error', err);
}
this.connection.publish(packet);
if (packet.qos === 1) {
this.inflight[packet.messageId] = packet;
}
}
forward(topic, payload, options, subTopic, qos, cb) {
if (options._dedupId <= this._lastDedupId) {
return;
}
this.logger.trace({ topic: topic }, 'delivering message');
const indexWildcard = subTopic.indexOf('#');
const indexPlus = subTopic.indexOf('+');
let forward = true;
const newId = this.nextId++;
this.nextId %= 65536;
const packet = {
topic,
payload,
qos,
messageId: newId,
};
if (qos) {
this.inflightCounter++;
}
if (this._closed || this._closing) {
this.logger.debug({ packet: packet }, 'trying to send a packet to a disconnected client');
forward = false;
}
else if (this.inflightCounter >= this.server.options.maxInflightMessages) {
this.logger.warn('too many inflight packets, closing');
this.close(null, 'too many inflight packets');
forward = false;
}
if (cb) {
cb();
}
forward = forward && !(topic.indexOf('$SYS') >= 0 && ((indexWildcard >= 0 && indexWildcard < 2) || (indexPlus >= 0 && indexPlus < 2)));
if (forward) {
if (options._dedupId === undefined) {
options._dedupId = this.server.nextDedupId();
this._lastDedupId = options._dedupId;
}
if (qos && options.messageId) {
this.server.updateOfflinePacket(this, options.messageId, packet, this.doForward);
}
else {
this.doForward(null, packet);
}
}
}
unsubscribeMapTo(topic, cb) {
const sub = this.subscriptions[topic];
if (!sub || !sub.handler) {
this.server.emit('unsubscribed', topic, this);
return cb();
}
this.server.listener.unsubscribe(topic, sub.handler, err => {
if (err) {
cb(err);
return;
}
if (!this._closing || this.clean) {
delete this.subscriptions[topic];
this.logger.info({ topic: topic }, 'unsubscribed');
this.server.emit('unsubscribed', topic, this);
}
cb();
});
}
handleConnect(packet, completeConnection) {
const client = this.connection;
this.id = packet.clientId;
this.logger = this.logger.child({ client: this });
if (!this.id) {
if (packet.protocolVersion == 4 && packet.clean) {
this.id = (0, uuid_1.v4)();
}
else {
this.logger.info('identifier rejected');
client.connack({
returnCode: 2,
});
client.stream.end();
return;
}
}
this.keepAlive = packet.keepalive;
this.will = packet.will;
this.clean = packet.clean;
if (this.id in this.server.clients) {
this.server.clients[this.id].close(completeConnection, 'new connection request');
}
else {
completeConnection();
}
}
handlePingReq() {
this.server.emit('pingReq', this);
}
handlePubAck(packet) {
const logger = this.logger;
logger.debug({ packet: packet }, 'pubAck');
if (this.inflight[packet.messageId]) {
this.server.emit('delivered', this.inflight[packet.messageId], this);
this.inflightCounter--;
delete this.inflight[packet.messageId];
}
else {
logger.info({ packet: packet }, 'no matching packet');
}
}
doSubscribe(s, cb) {
const handler = (topic, payload, options) => {
this.forward(topic, payload, options, s.topic, s.qos);
};
if (this.subscriptions[s.topic] === undefined) {
this.subscriptions[s.topic] = { qos: s.qos, handler: handler };
this.server.listener.subscribe(s.topic, handler, err => {
if (err) {
delete this.subscriptions[s.topic];
cb(err);
return;
}
this.logger.info({ topic: s.topic, qos: s.qos }, 'subscribed to topic');
this.subscriptions[s.topic] = { qos: s.qos, handler: handler };
cb(null, true);
});
}
else {
cb(null, true);
}
}
handleEachSub(s, cb) {
if (this.subscriptions[s.topic] === undefined) {
this.doSubscribe(s, cb);
}
else {
cb(null, true);
}
}
handleSubscribe(packet) {
const logger = this.logger;
logger.debug({ packet: packet }, 'subscribe received');
const granted = Client.calculateGranted(this, packet);
(0, steed_1.default)().map(this, packet.subscriptions, this.handleEachSub, (err, authorized) => {
if (err) {
this.close(null, err.message);
return;
}
packet.subscriptions.forEach((sub, index) => {
if (authorized[index]) {
this.server.emit('subscribed', sub.topic, this);
}
else {
granted[index] = 0x80;
}
});
if (!this._closed) {
this.connection.suback({
messageId: packet.messageId,
granted: granted,
});
}
});
}
handlePublish(packet) {
if (packet.qos === 2) {
switch (this.server.onQoS2publish) {
case 'dropToQoS1':
packet.qos = 1;
break;
case 'disconnect':
if (!this._closed && !this._closing) {
this.close(null, 'qos2 caused disconnect');
}
return;
default:
break;
}
}
const doPubAck = () => {
if (packet.qos === 1 && !(this._closed || this._closing)) {
this.connection.puback({
messageId: packet.messageId,
});
}
};
doPubAck();
}
onNonDisconnectClose(reason) {
const logger = this.logger;
const will = this.will;
if (this._closed || this._closing) {
return;
}
if (this.will) {
logger.info({ packet: will }, 'delivering last will');
setImmediate(() => {
this.handlePublish(will);
});
}
this.close(null, reason);
}
close(callback, reason) {
callback = callback || (() => { });
if (this._closed || this._closing) {
return callback();
}
if (this.id) {
this.logger.debug('closing client, reason: ' + reason);
if (this.timer) {
this.timer.clear();
}
}
const cleanup = () => {
this._closed = true;
this.logger.info('closed');
this.connection.removeAllListeners();
this.connection.on('error', function () { });
this.server.emit('clientDisconnected', this, reason);
callback();
};
this._closing = true;
steed_1.default.map(this, Object.keys(this.subscriptions), this.unsubscribeMapTo, err => {
if (err) {
this.logger.info(err);
}
if (!this._closed) {
cleanup();
if (this.connection.stream.destroySoon) {
this.connection.stream.destroySoon();
}
else if (this.connection.stream.destroy) {
this.connection.stream.destroy();
}
else {
this.connection.stream.end();
}
}
});
}
static calculateGranted(client, packet) {
return packet.subscriptions.map(function (e) {
if (e.qos === 2) {
e.qos = 1;
}
if (client.subscriptions[e.topic] !== undefined) {
client.subscriptions[e.topic].qos = e.qos;
}
return e.qos;
});
}
}
exports.Client = Client;
//# sourceMappingURL=client.js.map