amplify-appsync-simulator
Version:
An AppSync Simulator to test AppSync API.
190 lines • 8.01 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubscriptionServer = void 0;
const chalk_1 = __importDefault(require("chalk"));
const crypto_1 = __importDefault(require("crypto"));
const promise_toolbox_1 = require("promise-toolbox");
const http_1 = require("http");
const ip_1 = require("ip");
const util_1 = require("util");
const mqtt_server_1 = require("./subscription/mqtt-server");
const get_port_1 = __importDefault(require("get-port"));
const MINUTE = 1000 * 60;
const CONNECTION_TIMEOUT = 2 * MINUTE;
const TOPIC_EXPIRATION_TIMEOUT = 60 * MINUTE;
const BASE_PORT = 8900;
const MAX_PORT = 9999;
const log = console;
class SubscriptionServer {
constructor(config, appSyncServerContext) {
this.config = config;
this.appSyncServerContext = appSyncServerContext;
this.port = config.wsPort;
this.mqttWebSocketServer = (0, http_1.createServer)();
this.mqttServer = new mqtt_server_1.MQTTServer({
logger: {
level: process.env.DEBUG ? 'debug' : 'error',
},
});
this.mqttServer.attachHttpServer(this.mqttWebSocketServer);
this.clientRegistry = new Map();
this.mqttIteratorTimeout = new Map();
this.mqttServer.on('clientConnected', this.afterMQTTClientConnect.bind(this));
this.mqttServer.on('clientDisconnected', this.afterMQTTClientDisconnect.bind(this));
this.mqttServer.on('subscribed', this.afterSubscription.bind(this));
this.mqttServer.on('unsubscribed', this.afterMQTTClientUnsubscribe.bind(this));
this.realtimeSocketServer = (0, http_1.createServer)();
}
async start() {
if (!this.port) {
this.port = await (0, get_port_1.default)({
port: get_port_1.default.makeRange(BASE_PORT, MAX_PORT),
});
}
else {
try {
await (0, get_port_1.default)({
port: this.port,
});
}
catch (e) {
throw new Error(`Port ${this.port} is already in use. Please kill the program using this port and restart Mock`);
}
}
const server = this.mqttWebSocketServer.listen(this.port);
return await (0, promise_toolbox_1.fromEvent)(server, 'listening').then(() => {
const address = server.address();
this.url = `ws://${(0, ip_1.address)()}:${address.port}/`;
return server;
});
}
stop() {
if (this.mqttWebSocketServer) {
this.mqttWebSocketServer.close();
this.url = null;
this.mqttWebSocketServer = null;
}
}
async afterMQTTClientConnect(client) {
const { id: clientId } = client;
log.info(`Client (${chalk_1.default.bold(clientId)}) connected to subscription server`);
const timeout = this.mqttIteratorTimeout.get(client.id);
if (timeout) {
clearTimeout(timeout);
}
}
async afterSubscription(topic, client) {
const { id: clientId } = client;
log.info(`Client (${chalk_1.default.bold(clientId)}) subscribed to topic ${topic}`);
const regs = this.clientRegistry.get(clientId);
if (!regs) {
log.error(`No registration for client (${chalk_1.default.bold(clientId)})`);
return;
}
const reg = regs.find(({ topicId }) => topicId === topic);
if (!reg) {
log.error(`Client (${chalk_1.default.bold(clientId)}) tried to subscribe to non-existent topic ${topic}`);
return;
}
const { asyncIterator, topicId } = reg;
if (!reg.isRegistered) {
this.register(reg.document, reg.variables, reg.context, asyncIterator);
}
while (true) {
let { value: payload } = await asyncIterator.next();
log.info(`Publishing payload for topic ${topicId}`);
log.log('Payload:');
log.log((0, util_1.inspect)(payload));
this.mqttServer.publish({
topic: topicId,
payload: JSON.stringify(payload),
qos: 0,
retain: false,
});
}
}
afterMQTTClientUnsubscribe(topic, client) {
const { id: clientId } = client;
log.info(`Client (${chalk_1.default.bold(clientId)}) unsubscribed from topic ${topic}`);
const registration = this.clientRegistry.get(clientId);
if (!registration) {
log.error(`No registration for client (${chalk_1.default.bold(clientId)})`);
return;
}
const reg = registration.find(({ topicId }) => topicId === topic);
if (!reg) {
log.error(`Client (${chalk_1.default.bold(clientId)}) tried to unsubscribe from non-existent topic ${topic}`);
return;
}
reg.asyncIterator.return();
reg.isRegistered = false;
}
afterMQTTClientDisconnect(client) {
const { id: clientId } = client;
log.info(`Client (${chalk_1.default.bold(clientId)}) disconnected`);
const reg = this.clientRegistry.get(clientId);
if (!reg) {
log.error(`Unregistered client (${chalk_1.default.bold(clientId)}) disconnected`);
}
reg.forEach(subscription => {
subscription.asyncIterator.return();
});
this.clientRegistry.delete(clientId);
}
async register(document, variables, context, asyncIterator) {
const connection = context.request.connection;
const remoteAddress = `${connection.remoteAddress}:${connection.remotePort}`;
const clientId = crypto_1.default.createHash('MD5').update(remoteAddress).digest().toString('hex');
const subscriptionName = document.definitions[0].selectionSet.selections.find(s => s.kind === 'Field').name.value;
const paramHash = variables && Object.keys(variables).length
? crypto_1.default.createHash('MD5').update(JSON.stringify(variables)).digest().toString('hex')
: null;
const topicId = [clientId, subscriptionName, paramHash].join('/');
log.info(`Client (${chalk_1.default.bold(clientId)}) registered for topic ${topicId}`);
const registration = {
context,
document,
variables,
topicId,
asyncIterator: asyncIterator,
isRegistered: true,
};
const currentRegistrations = this.clientRegistry.get(clientId) || [];
const existingSubscription = currentRegistrations.find(reg => reg.topicId === topicId);
if (!existingSubscription) {
currentRegistrations.push(registration);
this.clientRegistry.set(clientId, currentRegistrations);
this.mqttIteratorTimeout.set(clientId, setTimeout(() => {
asyncIterator.return();
this.mqttIteratorTimeout.delete(clientId);
}, CONNECTION_TIMEOUT));
}
else {
Object.assign(existingSubscription, registration);
}
return {
extensions: {
subscription: {
mqttConnections: [
{
url: this.url,
topics: currentRegistrations.map(reg => reg.topicId),
client: clientId,
},
],
newSubscriptions: {
[subscriptionName]: {
topic: topicId,
expireTime: Date.now() + TOPIC_EXPIRATION_TIMEOUT,
},
},
},
},
};
}
}
exports.SubscriptionServer = SubscriptionServer;
//# sourceMappingURL=subscription.js.map