@gugananuvem/aws-local-simulator 1.0.27 → 1.0.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gugananuvem/aws-local-simulator",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Simulador local completo para serviços AWS",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -66,6 +66,6 @@
66
66
  "publishConfig": {
67
67
  "directory": "dist"
68
68
  },
69
- "buildDate": "2026-04-30T13:14:35.548Z",
69
+ "buildDate": "2026-04-30T19:05:54.245Z",
70
70
  "published": true
71
71
  }
@@ -77,8 +77,14 @@ class CognitoSimulator {
77
77
  return null;
78
78
  }
79
79
 
80
- const result = await this.lambdaSimulator.invoke(fnName, event, "RequestResponse");
81
- return result.Payload;
80
+ try {
81
+ const result = await this.lambdaSimulator.invoke(fnName, event, "RequestResponse");
82
+ return result.Payload;
83
+ } catch (error) {
84
+ const wrappedError = new Error(error.message);
85
+ wrappedError.code = "UserLambdaValidationException";
86
+ throw wrappedError;
87
+ }
82
88
  }
83
89
 
84
90
  async initialize() {
@@ -21,6 +21,20 @@ class DynamoDBSimulator {
21
21
  this.store = new LocalStore(this.dataDir);
22
22
  this.tables = new Map();
23
23
  this.audit = new CloudTrailAudit("dynamodb.amazonaws.com");
24
+ // Mutex por tabela para evitar race condition em escritas concorrentes
25
+ this._writeLocks = new Map();
26
+ }
27
+
28
+ /**
29
+ * Executa fn com exclusão mútua por tableName.
30
+ * Garante que escritas na mesma tabela não se sobreponham.
31
+ */
32
+ _withTableLock(tableName, fn) {
33
+ const prev = this._writeLocks.get(tableName) || Promise.resolve();
34
+ const next = prev.then(() => fn());
35
+ // Guarda apenas a tail da cadeia (sem acumular referências)
36
+ this._writeLocks.set(tableName, next.catch(() => {}));
37
+ return next;
24
38
  }
25
39
  async initialize() {
26
40
  logger.debug("Inicializando DynamoDB Simulator...");
@@ -179,10 +193,10 @@ class DynamoDBSimulator {
179
193
  case "DescribeTable": return this.describeTable(params.TableName);
180
194
  case "ListTables": return this.listTables(params);
181
195
  case "DeleteTable": return this.deleteTable(params);
182
- case "PutItem": return this.putItem(params);
196
+ case "PutItem": return this._withTableLock(params.TableName, () => this.putItem(params));
183
197
  case "GetItem": return this.getItem(params);
184
- case "UpdateItem": return this.updateItem(params);
185
- case "DeleteItem": return this.deleteItem(params);
198
+ case "UpdateItem": return this._withTableLock(params.TableName, () => this.updateItem(params));
199
+ case "DeleteItem": return this._withTableLock(params.TableName, () => this.deleteItem(params));
186
200
  case "BatchWriteItem": return this.batchWriteItem(params);
187
201
  case "BatchGetItem": return this.batchGetItem(params);
188
202
  case "Query": return this.query(params);
@@ -446,48 +460,67 @@ class DynamoDBSimulator {
446
460
  throw new Error(`RequestItems is required for BatchWriteItem. Params received: ${JSON.stringify(params)}`);
447
461
  }
448
462
 
449
- for (const [tableName, operations] of Object.entries(RequestItems)) {
450
- const table = this.tables.get(tableName);
451
- if (!table) continue;
452
-
453
- let items = this.store.read(tableName);
454
- const unprocessedItems = [];
455
-
456
- for (const op of operations) {
457
- if (op.PutRequest) {
458
- const item = this.normalizeItem(op.PutRequest.Item, table);
459
- const itemKey = this.getItemKey(item, table);
460
- const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
463
+ // Serializa por tabela usando o mutex para evitar race condition
464
+ const tablePromises = Object.entries(RequestItems).map(([tableName, operations]) =>
465
+ this._withTableLock(tableName, () => {
466
+ const table = this.tables.get(tableName);
467
+ if (!table) {
468
+ logger.info(`[BATCH-DEBUG] tabela não encontrada: ${tableName}`);
469
+ return;
470
+ }
461
471
 
462
- if (index !== -1) {
463
- items[index] = item;
464
- } else {
465
- items.push(item);
466
- table.itemCount++;
467
- }
468
- } else if (op.DeleteRequest) {
469
- const key = op.DeleteRequest.Key;
470
- const itemKey = this.getItemKeyFromKeys(key, table);
471
- const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
472
-
473
- if (index !== -1) {
474
- items.splice(index, 1);
475
- table.itemCount--;
476
- } else {
477
- unprocessedItems.push(op);
472
+ const itemsBefore = this.store.read(tableName);
473
+ logger.info(`[BATCH-DEBUG] ${tableName} | antes=${itemsBefore.length} | ops=${operations.length}`);
474
+
475
+ let items = [...itemsBefore];
476
+ const unprocessedItems = [];
477
+ let inserts = 0;
478
+ let updates = 0;
479
+
480
+ for (const op of operations) {
481
+ if (op.PutRequest) {
482
+ const item = this.normalizeItem(op.PutRequest.Item, table);
483
+ const itemKey = this.getItemKey(item, table);
484
+ const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
485
+
486
+ if (index !== -1) {
487
+ items[index] = item;
488
+ updates++;
489
+ } else {
490
+ items.push(item);
491
+ table.itemCount++;
492
+ inserts++;
493
+ }
494
+ } else if (op.DeleteRequest) {
495
+ const key = op.DeleteRequest.Key;
496
+ const itemKey = this.getItemKeyFromKeys(key, table);
497
+ const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
498
+
499
+ if (index !== -1) {
500
+ items.splice(index, 1);
501
+ table.itemCount--;
502
+ } else {
503
+ unprocessedItems.push(op);
504
+ }
478
505
  }
479
506
  }
480
- }
481
507
 
482
- this.store.write(tableName, items);
483
- if (unprocessedItems.length > 0) {
484
- responses[tableName] = unprocessedItems;
485
- }
486
- }
508
+ this.store.write(tableName, items);
509
+ const itemsAfter = this.store.read(tableName);
510
+ logger.info(`[BATCH-DEBUG] ${tableName} | depois=${itemsAfter.length} | esperado=${items.length} | match=${itemsAfter.length === items.length} | inserts=${inserts} | updates=${updates}`);
487
511
 
488
- this.persistTables();
512
+ if (unprocessedItems.length > 0) {
513
+ responses[tableName] = unprocessedItems;
514
+ }
515
+ })
516
+ );
489
517
 
490
- return { UnprocessedItems: responses };
518
+ return Promise.all(tablePromises).then(() => {
519
+ this.persistTables();
520
+ const finalCount = this.store.read(Object.keys(RequestItems)[0]).length;
521
+ //logger.info(`[BATCH-DEBUG] FINAL | tabela=${Object.keys(RequestItems)[0]} | total=${finalCount}`);
522
+ return { UnprocessedItems: responses };
523
+ });
491
524
  }
492
525
 
493
526
  batchGetItem(params) {
@@ -112,7 +112,13 @@ class LambdaSimulator {
112
112
  return { StatusCode: 202 };
113
113
  }
114
114
 
115
- const result = await this.executeHandler(lambda.handler, event);
115
+ let result;
116
+ try {
117
+ result = await this.executeHandler(lambda.handler, event);
118
+ } catch (error) {
119
+ logger.error(`❌ Lambda handler error (${functionName}):`, error);
120
+ throw error;
121
+ }
116
122
  this.audit.record({
117
123
  eventName: "Invoke",
118
124
  readOnly: false,
@@ -123,17 +129,9 @@ class LambdaSimulator {
123
129
  }
124
130
 
125
131
  async executeHandler(handler, event) {
126
- try {
127
- const context = this.createContext();
128
- const result = await handler(event, context);
129
- return result;
130
- } catch (error) {
131
- logger.error("❌ Erro no handler:", error);
132
- return {
133
- statusCode: 500,
134
- body: JSON.stringify({ error: "Internal Server Error", message: error.message }),
135
- };
136
- }
132
+ const context = this.createContext();
133
+ const result = await handler(event, context);
134
+ return result;
137
135
  }
138
136
 
139
137
  createContext() {