@gugananuvem/aws-local-simulator 1.0.34 â 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- package/aws-config +0 -154
|
@@ -11,17 +11,46 @@ class SQSSimulator {
|
|
|
11
11
|
constructor(config, lambdaService) {
|
|
12
12
|
this.config = config;
|
|
13
13
|
this.lambdaService = lambdaService;
|
|
14
|
+
this.snsService = null;
|
|
15
|
+
this.region = "local";
|
|
16
|
+
this.accountId = "000000000000";
|
|
14
17
|
this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, 'sqs');
|
|
15
18
|
this.store = new LocalStore(this.dataDir);
|
|
16
19
|
this.queues = new Map();
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
setLambdaService(lambdaService) {
|
|
23
|
+
this.lambdaService = lambdaService;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setSnsService(snsService) {
|
|
27
|
+
this.snsService = snsService;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setRegion(region) {
|
|
31
|
+
this.region = region;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setAccountId(accountId) {
|
|
35
|
+
this.accountId = accountId;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
async initialize() {
|
|
20
39
|
logger.debug('Inicializando SQS Simulator...');
|
|
21
40
|
this.loadQueues();
|
|
41
|
+
this.reattachLambdaHandlers();
|
|
22
42
|
logger.debug(`â
SQS Simulator inicializado com ${this.queues.size} filas`);
|
|
23
43
|
}
|
|
24
44
|
|
|
45
|
+
reattachLambdaHandlers() {
|
|
46
|
+
if (!this.lambdaService?.simulator) return;
|
|
47
|
+
for (const [queueName, queue] of this.queues.entries()) {
|
|
48
|
+
if (!queue.handler && queue.lambdaName) {
|
|
49
|
+
this.attachLambdaToQueue(queueName, queue.lambdaName, { batchSize: queue.batchSize });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
loadQueues() {
|
|
26
55
|
// Carrega filas da configuraÃ§ÃŖo
|
|
27
56
|
if (this.config.sqs?.queues) {
|
|
@@ -31,7 +60,11 @@ class SQSSimulator {
|
|
|
31
60
|
// Restore persisted messages for this queue
|
|
32
61
|
const queue = this.queues.get(name);
|
|
33
62
|
if (queue) {
|
|
34
|
-
|
|
63
|
+
const savedMessages = this.store.read(name) || [];
|
|
64
|
+
queue.messages = savedMessages.map(msg => ({
|
|
65
|
+
...msg,
|
|
66
|
+
messageReceiveTracking: new Map() // Reinitialize tracking map
|
|
67
|
+
}));
|
|
35
68
|
queue.messageCount = queue.messages.length;
|
|
36
69
|
}
|
|
37
70
|
}
|
|
@@ -43,7 +76,23 @@ class SQSSimulator {
|
|
|
43
76
|
for (const [name, data] of Object.entries(savedQueues)) {
|
|
44
77
|
if (!this.queues.has(name)) {
|
|
45
78
|
const messages = this.store.read(name) || [];
|
|
46
|
-
|
|
79
|
+
const queue = {
|
|
80
|
+
...data,
|
|
81
|
+
messages,
|
|
82
|
+
messageReceiveTracking: new Map(),
|
|
83
|
+
handler: null,
|
|
84
|
+
lambdaName: data.lambdaName || null
|
|
85
|
+
};
|
|
86
|
+
this.queues.set(name, queue);
|
|
87
|
+
} else {
|
|
88
|
+
// Merge with existing queue, preserving RedrivePolicy from saved state
|
|
89
|
+
const queue = this.queues.get(name);
|
|
90
|
+
if (data.redrivePolicy) {
|
|
91
|
+
queue.redrivePolicy = data.redrivePolicy;
|
|
92
|
+
}
|
|
93
|
+
if (data.isDLQ !== undefined) {
|
|
94
|
+
queue.isDLQ = data.isDLQ;
|
|
95
|
+
}
|
|
47
96
|
}
|
|
48
97
|
}
|
|
49
98
|
}
|
|
@@ -54,27 +103,56 @@ class SQSSimulator {
|
|
|
54
103
|
return { error: { code: 'QueueAlreadyExists', message: 'Queue already exists' }, status: 409 };
|
|
55
104
|
}
|
|
56
105
|
|
|
106
|
+
const region = attributes.Region || this.region || "local";
|
|
107
|
+
const accountId = attributes.AccountId || this.accountId || "000000000000";
|
|
108
|
+
|
|
57
109
|
const queue = {
|
|
58
110
|
name: queueName,
|
|
59
111
|
url: `http://localhost:${this.config.ports.sqs}/queue/${queueName}`,
|
|
60
|
-
arn: `arn:aws:sqs
|
|
112
|
+
arn: `arn:aws:sqs:${region}:${accountId}:${queueName}`,
|
|
61
113
|
messages: [],
|
|
62
114
|
handler: null,
|
|
115
|
+
lambdaName: null,
|
|
63
116
|
batchSize: parseInt(attributes.BatchSize || 10),
|
|
64
117
|
visibilityTimeout: parseInt(attributes.VisibilityTimeout || 30),
|
|
65
118
|
delaySeconds: parseInt(attributes.DelaySeconds || 0),
|
|
66
119
|
messageRetentionPeriod: parseInt(attributes.MessageRetentionPeriod || 345600),
|
|
120
|
+
|
|
121
|
+
// DLQ Support
|
|
122
|
+
redrivePolicy: null, // Will be set if RedrivePolicy is provided
|
|
123
|
+
isDLQ: false, // Flag to identify if this is a Dead Letter Queue
|
|
124
|
+
|
|
67
125
|
createdAt: new Date().toISOString(),
|
|
68
|
-
messageCount: 0
|
|
126
|
+
messageCount: 0,
|
|
127
|
+
|
|
128
|
+
// Track message receive counts for DLQ handling
|
|
129
|
+
messageReceiveTracking: new Map() // messageId -> { receiveCount, firstReceivedAt, lastReceivedAt, receiptHandles: Set }
|
|
69
130
|
};
|
|
70
131
|
|
|
132
|
+
this.queues.set(queueName, queue);
|
|
133
|
+
|
|
134
|
+
// Parse RedrivePolicy if provided
|
|
135
|
+
if (attributes.RedrivePolicy) {
|
|
136
|
+
try {
|
|
137
|
+
const policy = typeof attributes.RedrivePolicy === 'string'
|
|
138
|
+
? JSON.parse(attributes.RedrivePolicy)
|
|
139
|
+
: attributes.RedrivePolicy;
|
|
140
|
+
queue.redrivePolicy = {
|
|
141
|
+
deadLetterTargetArn: policy.deadLetterTargetArn,
|
|
142
|
+
maxReceiveCount: parseInt(policy.maxReceiveCount || 3)
|
|
143
|
+
};
|
|
144
|
+
logger.debug(`â
Fila ${queueName} configurada com DLQ: ${queue.redrivePolicy.deadLetterTargetArn}, maxReceiveCount: ${queue.redrivePolicy.maxReceiveCount}`);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
logger.warn(`â ī¸ RedrivePolicy invÃĄlida para ${queueName}: ${e.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
if (attributes.LambdaName) {
|
|
151
|
+
queue.lambdaName = attributes.LambdaName;
|
|
72
152
|
this.attachLambdaToQueue(queueName, attributes.LambdaName, { batchSize: queue.batchSize });
|
|
73
153
|
}
|
|
74
154
|
|
|
75
|
-
this.queues.set(queueName, queue);
|
|
76
155
|
this.persistQueues();
|
|
77
|
-
// Only initialize message store if it doesn't already exist
|
|
78
156
|
if (!this.store.read(queueName)) {
|
|
79
157
|
this.store.write(queueName, []);
|
|
80
158
|
}
|
|
@@ -90,6 +168,8 @@ class SQSSimulator {
|
|
|
90
168
|
return this.createQueueAction(req);
|
|
91
169
|
case 'SetQueueAttributes':
|
|
92
170
|
return this.setQueueAttributesAction(req);
|
|
171
|
+
case 'GetQueueAttributes':
|
|
172
|
+
return this.getQueueAttributesAction(req);
|
|
93
173
|
case 'SendMessage':
|
|
94
174
|
return this.sendMessageAction(req);
|
|
95
175
|
case 'SendMessageBatch':
|
|
@@ -117,10 +197,12 @@ class SQSSimulator {
|
|
|
117
197
|
}
|
|
118
198
|
|
|
119
199
|
// Parses Attribute.N.Name and Attribute.N.Value from request body/query
|
|
200
|
+
// Also supports direct attribute keys like RedrivePolicy
|
|
120
201
|
extractAttributes(req) {
|
|
121
202
|
const attributes = {};
|
|
122
|
-
const source = req.body.
|
|
203
|
+
const source = req.body && Object.keys(req.body).length > 0 ? req.body : req.query;
|
|
123
204
|
|
|
205
|
+
// First, look for Attribute.N.Name/Value pattern
|
|
124
206
|
Object.keys(source).forEach(key => {
|
|
125
207
|
if (key.startsWith('Attribute.') && key.endsWith('.Name')) {
|
|
126
208
|
const index = key.split('.')[1];
|
|
@@ -129,6 +211,15 @@ class SQSSimulator {
|
|
|
129
211
|
attributes[name] = value;
|
|
130
212
|
}
|
|
131
213
|
});
|
|
214
|
+
|
|
215
|
+
// Also support direct keys like RedrivePolicy, DelaySeconds, etc.
|
|
216
|
+
const directKeys = ['RedrivePolicy', 'DelaySeconds', 'VisibilityTimeout', 'MessageRetentionPeriod', 'BatchSize', 'LambdaName'];
|
|
217
|
+
directKeys.forEach(key => {
|
|
218
|
+
if (source[key] !== undefined) {
|
|
219
|
+
attributes[key] = source[key];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
132
223
|
return attributes;
|
|
133
224
|
}
|
|
134
225
|
|
|
@@ -158,11 +249,31 @@ class SQSSimulator {
|
|
|
158
249
|
if (attributes.VisibilityTimeout !== undefined) queue.visibilityTimeout = parseInt(attributes.VisibilityTimeout);
|
|
159
250
|
if (attributes.MessageRetentionPeriod !== undefined) queue.messageRetentionPeriod = parseInt(attributes.MessageRetentionPeriod);
|
|
160
251
|
|
|
252
|
+
// Support for RedrivePolicy (Dead Letter Queue configuration)
|
|
253
|
+
if (attributes.RedrivePolicy !== undefined) {
|
|
254
|
+
if (attributes.RedrivePolicy) {
|
|
255
|
+
try {
|
|
256
|
+
const policy = typeof attributes.RedrivePolicy === 'string'
|
|
257
|
+
? JSON.parse(attributes.RedrivePolicy)
|
|
258
|
+
: attributes.RedrivePolicy;
|
|
259
|
+
queue.redrivePolicy = {
|
|
260
|
+
deadLetterTargetArn: policy.deadLetterTargetArn,
|
|
261
|
+
maxReceiveCount: parseInt(policy.maxReceiveCount || 3)
|
|
262
|
+
};
|
|
263
|
+
logger.debug(`â
RedrivePolicy configurada para ${queueName}: ${JSON.stringify(queue.redrivePolicy)}`);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
logger.warn(`â ī¸ RedrivePolicy invÃĄlida para ${queueName}: ${e.message}`);
|
|
266
|
+
return { error: { code: 'InvalidParameterValue', message: 'Invalid RedrivePolicy format' }, status: 400 };
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
queue.redrivePolicy = null;
|
|
270
|
+
logger.debug(`â
RedrivePolicy removida de ${queueName}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
161
274
|
if (attributes.LambdaName !== undefined) {
|
|
162
275
|
queue.lambdaName = attributes.LambdaName;
|
|
163
276
|
if (attributes.LambdaName) {
|
|
164
|
-
// Here we'd normally re-attach if we had a direct reference to the lambda service function
|
|
165
|
-
// For now, we store the name and the server/index.js will handle re-attachment or we use the name to look it up
|
|
166
277
|
this.attachLambdaToQueue(queueName, attributes.LambdaName, { batchSize: queue.batchSize });
|
|
167
278
|
} else {
|
|
168
279
|
queue.handler = null;
|
|
@@ -177,6 +288,36 @@ class SQSSimulator {
|
|
|
177
288
|
return { success: true };
|
|
178
289
|
}
|
|
179
290
|
|
|
291
|
+
getQueueAttributesAction(req) {
|
|
292
|
+
const queueName = this.resolveQueueName(req);
|
|
293
|
+
const queue = this.queues.get(queueName);
|
|
294
|
+
|
|
295
|
+
if (!queue) {
|
|
296
|
+
return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Build response with all attributes
|
|
300
|
+
const attributes = {
|
|
301
|
+
DelaySeconds: String(queue.delaySeconds),
|
|
302
|
+
VisibilityTimeout: String(queue.visibilityTimeout),
|
|
303
|
+
MessageRetentionPeriod: String(queue.messageRetentionPeriod),
|
|
304
|
+
ApproximateNumberOfMessages: String(queue.messageCount),
|
|
305
|
+
ApproximateNumberOfMessagesNotVisible: String(
|
|
306
|
+
queue.messages.filter(m => m.invisibleUntil && m.invisibleUntil > Date.now()).length
|
|
307
|
+
),
|
|
308
|
+
CreatedTimestamp: String(Math.floor(new Date(queue.createdAt).getTime() / 1000)),
|
|
309
|
+
QueueArn: queue.arn
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Add RedrivePolicy if configured
|
|
313
|
+
if (queue.redrivePolicy) {
|
|
314
|
+
attributes.RedrivePolicy = JSON.stringify(queue.redrivePolicy);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { attributes };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
180
321
|
sendMessageAction(req) {
|
|
181
322
|
const queueName = this.resolveQueueName(req);
|
|
182
323
|
const queue = this.queues.get(queueName);
|
|
@@ -186,21 +327,62 @@ class SQSSimulator {
|
|
|
186
327
|
}
|
|
187
328
|
|
|
188
329
|
const messageBody = req.query.MessageBody || req.body.MessageBody;
|
|
330
|
+
return this.sendMessage({ QueueUrl: queue.url, MessageBody: messageBody });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
sendMessage(params) {
|
|
334
|
+
const { QueueUrl, MessageBody } = params;
|
|
335
|
+
if (!QueueUrl || !MessageBody) {
|
|
336
|
+
return { error: { code: 'MissingParameter', message: 'QueueUrl and MessageBody are required' }, status: 400 };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let queueName;
|
|
340
|
+
const match = QueueUrl.match(/\/queue\/([^/?]+)/);
|
|
341
|
+
if (match) {
|
|
342
|
+
queueName = match[1];
|
|
343
|
+
} else {
|
|
344
|
+
const arnMatch = QueueUrl.match(/:([^:]+)$/);
|
|
345
|
+
queueName = arnMatch ? arnMatch[1] : QueueUrl;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const queue = this.queues.get(queueName);
|
|
349
|
+
if (!queue) {
|
|
350
|
+
return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
|
|
351
|
+
}
|
|
352
|
+
|
|
189
353
|
const messageId = crypto.randomUUID();
|
|
190
354
|
const receiptHandle = crypto.randomUUID();
|
|
191
|
-
const md5 = crypto.createHash('md5').update(
|
|
355
|
+
const md5 = crypto.createHash('md5').update(MessageBody).digest('hex');
|
|
356
|
+
const nowMs = Date.now();
|
|
192
357
|
|
|
193
358
|
const message = {
|
|
194
359
|
MessageId: messageId,
|
|
195
360
|
ReceiptHandle: receiptHandle,
|
|
196
|
-
Body:
|
|
361
|
+
Body: MessageBody,
|
|
197
362
|
MD5OfBody: md5,
|
|
198
|
-
Attributes: {
|
|
199
|
-
|
|
363
|
+
Attributes: {
|
|
364
|
+
ApproximateReceiveCount: "0", // Will be incremented when received
|
|
365
|
+
SentTimestamp: String(nowMs),
|
|
366
|
+
ApproximateFirstReceiveTimestamp: null,
|
|
367
|
+
SenderId: "AIDAJDPLRKLG7UEXAMPLE",
|
|
368
|
+
AWSTraceHeader: null
|
|
369
|
+
},
|
|
370
|
+
timestamp: nowMs,
|
|
371
|
+
receiveCount: 0, // Track internally how many times received
|
|
372
|
+
invisibleUntil: null // When message becomes visible again
|
|
200
373
|
};
|
|
201
374
|
|
|
202
375
|
queue.messages.push(message);
|
|
203
376
|
queue.messageCount++;
|
|
377
|
+
|
|
378
|
+
// Initialize receive tracking for this message
|
|
379
|
+
queue.messageReceiveTracking.set(messageId, {
|
|
380
|
+
receiveCount: 0,
|
|
381
|
+
firstReceivedAt: null,
|
|
382
|
+
lastReceivedAt: null,
|
|
383
|
+
receiptHandles: new Set()
|
|
384
|
+
});
|
|
385
|
+
|
|
204
386
|
this.persistQueue(queueName);
|
|
205
387
|
|
|
206
388
|
logger.verboso(`đ¨ Mensagem enviada para ${queueName}: ${messageId}`);
|
|
@@ -276,14 +458,54 @@ class SQSSimulator {
|
|
|
276
458
|
}
|
|
277
459
|
|
|
278
460
|
const maxNumberOfMessages = parseInt(req.query.MaxNumberOfMessages || req.body.MaxNumberOfMessages || 1);
|
|
279
|
-
const
|
|
461
|
+
const nowMs = Date.now();
|
|
462
|
+
|
|
463
|
+
// Collect visible messages (don't remove yet)
|
|
464
|
+
const messagesToReturn = [];
|
|
465
|
+
for (let i = 0; i < queue.messages.length && messagesToReturn.length < maxNumberOfMessages; i++) {
|
|
466
|
+
const msg = queue.messages[i];
|
|
467
|
+
if (!msg.invisibleUntil || msg.invisibleUntil <= nowMs) {
|
|
468
|
+
messagesToReturn.push(msg);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
280
471
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this
|
|
472
|
+
// Process each message
|
|
473
|
+
for (const message of messagesToReturn) {
|
|
474
|
+
// Generate new ReceiptHandle for this receive (unique per receive)
|
|
475
|
+
const newReceiptHandle = crypto.randomUUID();
|
|
476
|
+
message.ReceiptHandle = newReceiptHandle;
|
|
477
|
+
|
|
478
|
+
// Increment receive count
|
|
479
|
+
message.receiveCount++;
|
|
480
|
+
message.Attributes.ApproximateReceiveCount = String(message.receiveCount);
|
|
481
|
+
|
|
482
|
+
// Set approximate first receive timestamp on first receive
|
|
483
|
+
if (!message.Attributes.ApproximateFirstReceiveTimestamp || message.Attributes.ApproximateFirstReceiveTimestamp === "null") {
|
|
484
|
+
message.Attributes.ApproximateFirstReceiveTimestamp = String(nowMs);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Set visibility timeout (make invisible for next visibility timeout seconds)
|
|
488
|
+
message.invisibleUntil = nowMs + (queue.visibilityTimeout * 1000);
|
|
489
|
+
|
|
490
|
+
// Update tracking
|
|
491
|
+
const tracking = queue.messageReceiveTracking.get(message.MessageId);
|
|
492
|
+
if (tracking) {
|
|
493
|
+
tracking.receiveCount = message.receiveCount;
|
|
494
|
+
tracking.lastReceivedAt = nowMs;
|
|
495
|
+
if (!tracking.firstReceivedAt) {
|
|
496
|
+
tracking.firstReceivedAt = nowMs;
|
|
497
|
+
}
|
|
498
|
+
tracking.receiptHandles.add(newReceiptHandle);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
logger.verboso(`đŦ Mensagem recebida ${message.MessageId} da fila ${queueName} (receiveCount: ${message.receiveCount})`);
|
|
284
502
|
}
|
|
285
503
|
|
|
286
|
-
|
|
504
|
+
// Update message count
|
|
505
|
+
queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
|
|
506
|
+
this.persistQueue(queueName);
|
|
507
|
+
|
|
508
|
+
return { messages: messagesToReturn };
|
|
287
509
|
}
|
|
288
510
|
|
|
289
511
|
deleteMessageAction(req) {
|
|
@@ -295,11 +517,15 @@ class SQSSimulator {
|
|
|
295
517
|
return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
|
|
296
518
|
}
|
|
297
519
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
520
|
+
// Find message by current ReceiptHandle (which was set at receive time)
|
|
521
|
+
const messageIndex = queue.messages.findIndex(m => m.ReceiptHandle === receiptHandle);
|
|
522
|
+
if (messageIndex !== -1) {
|
|
523
|
+
const deletedMessage = queue.messages.splice(messageIndex, 1)[0];
|
|
301
524
|
queue.messageCount--;
|
|
525
|
+
logger.verboso(`đī¸ Mensagem deletada: ${deletedMessage.MessageId} da fila ${queueName}`);
|
|
302
526
|
this.persistQueue(queueName);
|
|
527
|
+
} else {
|
|
528
|
+
logger.warn(`â ī¸ Mensagem com ReceiptHandle ${receiptHandle} nÃŖo encontrada em ${queueName}`);
|
|
303
529
|
}
|
|
304
530
|
|
|
305
531
|
return { success: true };
|
|
@@ -326,6 +552,16 @@ class SQSSimulator {
|
|
|
326
552
|
return { queues };
|
|
327
553
|
}
|
|
328
554
|
|
|
555
|
+
_extractLambdaFunctionName(lambdaRef) {
|
|
556
|
+
if (!lambdaRef || typeof lambdaRef === 'function') return null;
|
|
557
|
+
if (typeof lambdaRef !== 'string') return null;
|
|
558
|
+
const lambdaStr = lambdaRef.trim();
|
|
559
|
+
const arnMatch = lambdaStr.match(/arn:aws:lambda:[^:]+:[^:]+:function:([^:]+)/);
|
|
560
|
+
if (arnMatch) return arnMatch[1];
|
|
561
|
+
if (lambdaStr.startsWith('function:')) return lambdaStr.slice(9);
|
|
562
|
+
return lambdaStr;
|
|
563
|
+
}
|
|
564
|
+
|
|
329
565
|
attachLambdaToQueue(queueName, handler, options = {}) {
|
|
330
566
|
const queue = this.queues.get(queueName);
|
|
331
567
|
|
|
@@ -334,31 +570,131 @@ class SQSSimulator {
|
|
|
334
570
|
return this.attachLambdaToQueue(queueName, handler, options);
|
|
335
571
|
}
|
|
336
572
|
|
|
337
|
-
|
|
573
|
+
let resolvedHandler = handler;
|
|
574
|
+
|
|
575
|
+
if (typeof handler === 'string') {
|
|
576
|
+
const functionName = this._extractLambdaFunctionName(handler);
|
|
577
|
+
queue.lambdaName = functionName || handler;
|
|
578
|
+
|
|
579
|
+
if (this.lambdaService?.simulator) {
|
|
580
|
+
const lambdaSim = this.lambdaService.simulator;
|
|
581
|
+
if (functionName) {
|
|
582
|
+
resolvedHandler = async (event) => {
|
|
583
|
+
return lambdaSim.invoke(functionName, event, "Event");
|
|
584
|
+
};
|
|
585
|
+
logger.debug(`đ Lambda "${functionName}" (resolved via ARN/nome) associada à fila ${queueName}`);
|
|
586
|
+
} else {
|
|
587
|
+
resolvedHandler = async (event) => {
|
|
588
|
+
return lambdaSim.invoke(handler, event, "Event");
|
|
589
|
+
};
|
|
590
|
+
logger.debug(`đ Lambda "${handler}" associada à fila ${queueName}`);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
logger.warn(`â ī¸ Lambda service nÃŖo disponÃvel para resolver ${handler} na fila ${queueName}`);
|
|
594
|
+
resolvedHandler = null;
|
|
595
|
+
}
|
|
596
|
+
} else if (typeof handler === 'function') {
|
|
597
|
+
queue.lambdaName = options?.lambdaName || queue.lambdaName;
|
|
598
|
+
logger.debug(`đ Handler direto associado à fila ${queueName}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
queue.handler = resolvedHandler;
|
|
338
602
|
queue.batchSize = options.batchSize || 10;
|
|
339
603
|
this.persistQueue(queueName);
|
|
340
604
|
|
|
341
|
-
logger.debug(`đ Lambda associada à fila ${queueName}`);
|
|
342
|
-
|
|
343
605
|
return queue;
|
|
344
606
|
}
|
|
345
607
|
|
|
608
|
+
async moveMessageToDLQ(queue, message) {
|
|
609
|
+
if (!queue.redrivePolicy) {
|
|
610
|
+
logger.debug(`â ī¸ Nenhuma DLQ configurada para ${queue.name}, mensagem ${message.MessageId} serÃĄ descartada`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const dlqArn = queue.redrivePolicy.deadLetterTargetArn;
|
|
615
|
+
const dlqName = dlqArn.split(':').pop();
|
|
616
|
+
const dlq = this.queues.get(dlqName);
|
|
617
|
+
|
|
618
|
+
if (!dlq) {
|
|
619
|
+
logger.error(`â DLQ nÃŖo encontrada: ${dlqName}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Create new message in DLQ with original body and metadata
|
|
624
|
+
const dlqMessage = {
|
|
625
|
+
MessageId: crypto.randomUUID(),
|
|
626
|
+
ReceiptHandle: crypto.randomUUID(),
|
|
627
|
+
Body: message.Body,
|
|
628
|
+
MD5OfBody: message.MD5OfBody,
|
|
629
|
+
Attributes: {
|
|
630
|
+
...message.Attributes,
|
|
631
|
+
ApproximateReceiveCount: "0", // Reset in DLQ
|
|
632
|
+
ApproximateFirstReceiveTimestamp: null
|
|
633
|
+
},
|
|
634
|
+
timestamp: Date.now(),
|
|
635
|
+
receiveCount: 0,
|
|
636
|
+
invisibleUntil: null,
|
|
637
|
+
originalMessageId: message.MessageId // Reference to original
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
dlq.messages.push(dlqMessage);
|
|
641
|
+
dlq.messageCount++;
|
|
642
|
+
|
|
643
|
+
dlq.messageReceiveTracking.set(dlqMessage.MessageId, {
|
|
644
|
+
receiveCount: 0,
|
|
645
|
+
firstReceivedAt: null,
|
|
646
|
+
lastReceivedAt: null,
|
|
647
|
+
receiptHandles: new Set()
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
logger.warn(`đ Mensagem ${message.MessageId} movida para DLQ: ${dlqName}`);
|
|
651
|
+
this.persistQueue(dlqName);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async checkAndMoveToDLQ(queue, message) {
|
|
655
|
+
if (!queue.redrivePolicy) return false;
|
|
656
|
+
|
|
657
|
+
const maxReceiveCount = queue.redrivePolicy.maxReceiveCount;
|
|
658
|
+
const receiveCount = parseInt(message.Attributes.ApproximateReceiveCount || 0);
|
|
659
|
+
|
|
660
|
+
if (receiveCount >= maxReceiveCount) {
|
|
661
|
+
await this.moveMessageToDLQ(queue, message);
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
|
|
346
668
|
async processQueueMessages(queue) {
|
|
347
669
|
if (!queue.handler || queue.messages.length === 0) return;
|
|
348
670
|
|
|
349
|
-
const
|
|
671
|
+
const visibleMessages = queue.messages.filter(m =>
|
|
672
|
+
!m.invisibleUntil || m.invisibleUntil <= Date.now()
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
if (visibleMessages.length === 0) return;
|
|
676
|
+
|
|
677
|
+
const messagesToProcess = visibleMessages.splice(0, queue.batchSize);
|
|
678
|
+
const nowMs = Date.now();
|
|
350
679
|
|
|
351
680
|
const event = {
|
|
352
681
|
Records: messagesToProcess.map(msg => ({
|
|
353
682
|
messageId: msg.MessageId,
|
|
354
683
|
receiptHandle: msg.ReceiptHandle,
|
|
355
684
|
body: msg.Body,
|
|
356
|
-
attributes:
|
|
685
|
+
attributes: {
|
|
686
|
+
ApproximateReceiveCount: msg.Attributes.ApproximateReceiveCount || "0",
|
|
687
|
+
SentTimestamp: String(msg.Attributes.SentTimestamp || msg.timestamp || nowMs),
|
|
688
|
+
SenderId: msg.Attributes.SenderId || "AIDAJDPLRKLG7UEXAMPLE",
|
|
689
|
+
ApproximateFirstReceiveTimestamp: msg.Attributes.ApproximateFirstReceiveTimestamp || String(nowMs),
|
|
690
|
+
AWSTraceHeader: msg.Attributes?.AWSTraceHeader,
|
|
691
|
+
},
|
|
357
692
|
messageAttributes: {},
|
|
358
693
|
md5OfBody: msg.MD5OfBody,
|
|
694
|
+
md5OfMessageAttributes: null,
|
|
359
695
|
eventSource: 'aws:sqs',
|
|
360
696
|
eventSourceARN: queue.arn,
|
|
361
|
-
awsRegion: 'local'
|
|
697
|
+
awsRegion: this.region || 'local'
|
|
362
698
|
}))
|
|
363
699
|
};
|
|
364
700
|
|
|
@@ -368,16 +704,69 @@ class SQSSimulator {
|
|
|
368
704
|
const result = await queue.handler(event);
|
|
369
705
|
|
|
370
706
|
if (result && result.batchItemFailures && result.batchItemFailures.length > 0) {
|
|
707
|
+
// Only failed messages are returned to the queue
|
|
371
708
|
const failedIds = new Set(result.batchItemFailures.map(f => f.itemIdentifier));
|
|
372
709
|
const failedMessages = messagesToProcess.filter(msg => failedIds.has(msg.MessageId));
|
|
373
|
-
|
|
374
|
-
|
|
710
|
+
const successMessages = messagesToProcess.filter(msg => !failedIds.has(msg.MessageId));
|
|
711
|
+
|
|
712
|
+
// Remove successfully processed messages from queue
|
|
713
|
+
for (const successMsg of successMessages) {
|
|
714
|
+
const idx = queue.messages.findIndex(m => m.MessageId === successMsg.MessageId);
|
|
715
|
+
if (idx !== -1) {
|
|
716
|
+
queue.messages.splice(idx, 1);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Return failed messages to queue (will be reprocessed after visibility timeout)
|
|
721
|
+
for (const msg of failedMessages) {
|
|
722
|
+
// Check if should go to DLQ
|
|
723
|
+
const shouldMoveToDLQ = await this.checkAndMoveToDLQ(queue, msg);
|
|
724
|
+
if (!shouldMoveToDLQ) {
|
|
725
|
+
// Message already in queue, just reset invisibility for retry
|
|
726
|
+
msg.invisibleUntil = 0;
|
|
727
|
+
logger.verboso(`âŠī¸ Mensagem ${msg.MessageId} retornada à fila ${queue.name} (tentativa ${msg.receiveCount + 1})`);
|
|
728
|
+
} else {
|
|
729
|
+
// Message was moved to DLQ, remove it from current queue
|
|
730
|
+
const idx = queue.messages.findIndex(m => m.MessageId === msg.MessageId);
|
|
731
|
+
if (idx !== -1) {
|
|
732
|
+
queue.messages.splice(idx, 1);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Update message count
|
|
738
|
+
queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
|
|
739
|
+
|
|
740
|
+
logger.warn(`â ī¸ ${failedMessages.length} mensagens falharam na fila ${queue.name}, ${successMessages.length} processadas com sucesso`);
|
|
375
741
|
} else {
|
|
376
|
-
|
|
742
|
+
// If no explicit batchItemFailures, treat all as successful (delete them)
|
|
743
|
+
for (const msg of messagesToProcess) {
|
|
744
|
+
const idx = queue.messages.findIndex(m => m.MessageId === msg.MessageId);
|
|
745
|
+
if (idx !== -1) {
|
|
746
|
+
queue.messages.splice(idx, 1);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
logger.verboso(`â
${messagesToProcess.length} mensagens da fila ${queue.name} processadas com sucesso`);
|
|
750
|
+
queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
|
|
377
751
|
}
|
|
378
752
|
} catch (error) {
|
|
379
|
-
logger.error(`â Erro ao processar mensagens:`, error);
|
|
380
|
-
|
|
753
|
+
logger.error(`â Erro ao processar mensagens da fila ${queue.name}:`, error);
|
|
754
|
+
|
|
755
|
+
// On exception, return all messages to queue for retry
|
|
756
|
+
for (const msg of messagesToProcess) {
|
|
757
|
+
const shouldMoveToDLQ = await this.checkAndMoveToDLQ(queue, msg);
|
|
758
|
+
if (!shouldMoveToDLQ) {
|
|
759
|
+
// Message already in queue, just reset invisibility for retry
|
|
760
|
+
msg.invisibleUntil = 0;
|
|
761
|
+
} else {
|
|
762
|
+
// Message was moved to DLQ, remove it from current queue
|
|
763
|
+
const idx = queue.messages.findIndex(m => m.MessageId === msg.MessageId);
|
|
764
|
+
if (idx !== -1) {
|
|
765
|
+
queue.messages.splice(idx, 1);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
|
|
381
770
|
}
|
|
382
771
|
|
|
383
772
|
this.persistQueue(queue.name);
|
|
@@ -395,10 +784,11 @@ class SQSSimulator {
|
|
|
395
784
|
delaySeconds: queue.delaySeconds,
|
|
396
785
|
messageRetentionPeriod: queue.messageRetentionPeriod,
|
|
397
786
|
lambdaName: queue.lambdaName,
|
|
787
|
+
redrivePolicy: queue.redrivePolicy, // Persist RedrivePolicy
|
|
788
|
+
isDLQ: queue.isDLQ,
|
|
398
789
|
createdAt: queue.createdAt,
|
|
399
790
|
messageCount: queue.messageCount
|
|
400
791
|
};
|
|
401
|
-
|
|
402
792
|
}
|
|
403
793
|
this.store.write('__queues__', queuesObj);
|
|
404
794
|
}
|