serverless-offline-sqs
Version:
Emulate AWS λ and SQS locally when developing your Serverless project
179 lines (147 loc) • 4.51 kB
JavaScript
const SQSClient = require('aws-sdk/clients/sqs');
const {
chunk,
find,
get,
isPlainObject,
mapValues,
matches,
pipe,
toString,
values
} = require('lodash/fp');
const log = require('@serverless/utils/log').log;
const {default: PQueue} = require('p-queue');
const SQSEventDefinition = require('./sqs-event-definition');
const SQSEvent = require('./sqs-event');
const delay = timeout =>
new Promise(resolve => {
setTimeout(resolve, timeout);
});
class SQS {
constructor(lambda, resources, options) {
this.lambda = null;
this.resources = null;
this.options = null;
this.lambda = lambda;
this.resources = resources;
this.options = options;
this.client = new SQSClient(this.options);
this.queue = new PQueue({autoStart: false});
}
create(events) {
return Promise.all(events.map(({functionKey, sqs}) => this._create(functionKey, sqs)));
}
start() {
this.queue.start();
}
stop(timeout) {
this.queue.pause();
}
_create(functionKey, rawSqsEventDefinition) {
const sqsEvent = new SQSEventDefinition(
rawSqsEventDefinition,
this.options.region,
this.options.accountId
);
return this._sqsEvent(functionKey, sqsEvent);
}
_rewriteQueueUrl(queueUrl) {
if (!this.options.endpoint) return queueUrl;
const {hostname, protocol, username, password, port} = new URL(this.options.endpoint);
const rewritedQueueUrl = new URL(queueUrl);
rewritedQueueUrl.hostname = hostname;
rewritedQueueUrl.protocol = protocol;
rewritedQueueUrl.username = username;
rewritedQueueUrl.password = password;
rewritedQueueUrl.port = port;
return rewritedQueueUrl.href;
}
async _getQueueUrl(queueName) {
try {
return await this.client.getQueueUrl({QueueName: queueName}).promise();
} catch (err) {
await delay(10000);
return this._getQueueUrl(queueName);
}
}
async _sqsEvent(functionKey, sqsEvent) {
const {enabled, arn, queueName, batchSize = 10} = sqsEvent;
if (!enabled) return;
if (this.options.autoCreate) await this._createQueue(sqsEvent);
const QueueUrl = this._rewriteQueueUrl(
(await this.client.getQueueUrl({QueueName: queueName}).promise()).QueueUrl
);
const getMessages = async (size, messages = []) => {
if (size <= 0) return messages;
const {Messages} = await this.client
.receiveMessage({
QueueUrl,
MaxNumberOfMessages: size > 10 ? 10 : size,
AttributeNames: ['All'],
MessageAttributeNames: ['All'],
WaitTimeSeconds: 5
})
.promise();
if (!Messages || Messages.length === 0) return messages;
return getMessages(size - Messages.length, [...messages, ...Messages]);
};
const job = async () => {
const messages = await getMessages(batchSize);
if (messages.length > 0) {
try {
const lambdaFunction = this.lambda.get(functionKey);
const event = new SQSEvent(messages, this.region, arn);
lambdaFunction.setEvent(event);
await lambdaFunction.runHandler();
await Promise.all(
chunk(
10,
(messages || []).map(({MessageId: Id, ReceiptHandle}) => ({
Id,
ReceiptHandle
}))
).map(Entries =>
this.client
.deleteMessageBatch({
Entries,
QueueUrl
})
.promise()
)
);
} catch (err) {
log.warning(err.stack);
}
}
this.queue.add(job);
};
this.queue.add(job);
}
_getResourceProperties(queueName) {
return pipe(
values,
find(matches({Properties: {QueueName: queueName}})),
get('Properties')
)(this.resources);
}
async _createQueue({queueName}, remainingTry = 5) {
try {
const properties = this._getResourceProperties(queueName);
await this.client
.createQueue({
QueueName: queueName,
Attributes: mapValues(
value => (isPlainObject(value) ? JSON.stringify(value) : toString(value)),
properties
)
})
.promise();
} catch (err) {
if (remainingTry > 0 && err.name === 'AWS.SimpleQueueService.NonExistentQueue')
return this._createQueue({queueName}, remainingTry - 1);
log.warning(err.stack);
}
}
}
module.exports = SQS;