@gugananuvem/aws-local-simulator 1.0.26 → 1.0.28

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.26",
3
+ "version": "1.0.28",
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-27T11:03:00.202Z",
69
+ "buildDate": "2026-04-30T14:28:21.971Z",
70
70
  "published": true
71
71
  }
@@ -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,46 +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
- responses[tableName] = { UnprocessedItems: unprocessedItems };
484
- }
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}`);
485
511
 
486
- this.persistTables();
512
+ if (unprocessedItems.length > 0) {
513
+ responses[tableName] = unprocessedItems;
514
+ }
515
+ })
516
+ );
487
517
 
488
- 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
+ });
489
524
  }
490
525
 
491
526
  batchGetItem(params) {