diff --git a/timeless-api/pom.xml b/timeless-api/pom.xml
index b979a85..38a48d5 100644
--- a/timeless-api/pom.xml
+++ b/timeless-api/pom.xml
@@ -67,6 +67,10 @@
quarkus-langchain4j-openai
${quarkus-langchain4j-openai.version}
+
+ io.quarkiverse.amazonservices
+ quarkus-messaging-amazon-sqs
+
io.quarkiverse.amazonservices
quarkus-amazon-sqs
diff --git a/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessage.java b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessage.java
new file mode 100644
index 0000000..310ea4a
--- /dev/null
+++ b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessage.java
@@ -0,0 +1,101 @@
+package dev.matheuscruz.infra.outbox;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.time.Instant;
+import java.util.UUID;
+
+@Entity
+@Table(name = "outbox_messages")
+public class OutboxMessage {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ @Column(nullable = false, columnDefinition = "TEXT")
+ private String payload;
+
+ @Column(name = "queue_url", nullable = false)
+ private String queueUrl;
+
+ @Column(name = "message_group_id", nullable = false)
+ private String messageGroupId;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private OutboxStatus status;
+
+ @Column(name = "created_at", nullable = false)
+ private Instant createdAt;
+
+ @Column(name = "processed_at")
+ private Instant processedAt;
+
+ @Column(name = "retry_count", nullable = false)
+ private int retryCount;
+
+ protected OutboxMessage() {
+ }
+
+ public OutboxMessage(String payload, String queueUrl, String messageGroupId) {
+ this.payload = payload;
+ this.queueUrl = queueUrl;
+ this.messageGroupId = messageGroupId;
+ this.status = OutboxStatus.PENDING;
+ this.createdAt = Instant.now();
+ this.retryCount = 0;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public String getPayload() {
+ return payload;
+ }
+
+ public String getQueueUrl() {
+ return queueUrl;
+ }
+
+ public String getMessageGroupId() {
+ return messageGroupId;
+ }
+
+ public OutboxStatus getStatus() {
+ return status;
+ }
+
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ public Instant getProcessedAt() {
+ return processedAt;
+ }
+
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ public void markAsSent() {
+ this.status = OutboxStatus.SENT;
+ this.processedAt = Instant.now();
+ }
+
+ public void markAsFailed() {
+ this.status = OutboxStatus.FAILED;
+ this.processedAt = Instant.now();
+ }
+
+ public void incrementRetryCount() {
+ this.retryCount++;
+ }
+}
diff --git a/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessageRelay.java b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessageRelay.java
new file mode 100644
index 0000000..9f2d901
--- /dev/null
+++ b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessageRelay.java
@@ -0,0 +1,70 @@
+package dev.matheuscruz.infra.outbox;
+
+import io.quarkus.narayana.jta.QuarkusTransaction;
+import io.quarkus.scheduler.Scheduled;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.List;
+import org.jboss.logging.Logger;
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
+
+@ApplicationScoped
+public class OutboxMessageRelay {
+
+ private static final int MAX_RETRIES = 10;
+
+ private final OutboxMessageRepository outboxMessageRepository;
+ private final SqsClient sqsClient;
+ private final Logger logger = Logger.getLogger(OutboxMessageRelay.class);
+
+ public OutboxMessageRelay(OutboxMessageRepository outboxMessageRepository, SqsClient sqsClient) {
+ this.outboxMessageRepository = outboxMessageRepository;
+ this.sqsClient = sqsClient;
+ }
+
+ @Scheduled(every = "5s")
+ public void processOutbox() {
+ List pending = outboxMessageRepository.findPendingMessages();
+
+ for (OutboxMessage message : pending) {
+ if (message.getRetryCount() >= MAX_RETRIES) {
+ failMessage(message);
+ continue;
+ }
+
+ try {
+ sqsClient.sendMessage(SendMessageRequest.builder().queueUrl(message.getQueueUrl())
+ .messageBody(message.getPayload()).messageGroupId(message.getMessageGroupId()).build());
+
+ updateMessageStatus(message, true);
+ } catch (Exception e) {
+ logger.errorf(e, "Failed to send outbox message %s to queue %s", message.getId(),
+ message.getQueueUrl());
+ updateMessageStatus(message, false);
+ }
+ }
+ }
+
+ private void updateMessageStatus(OutboxMessage message, boolean success) {
+ QuarkusTransaction.requiringNew().run(() -> {
+ OutboxMessage managed = outboxMessageRepository.findById(message.getId());
+ if (managed != null) {
+ if (success) {
+ managed.markAsSent();
+ } else {
+ managed.incrementRetryCount();
+ }
+ }
+ });
+ }
+
+ private void failMessage(OutboxMessage message) {
+ QuarkusTransaction.requiringNew().run(() -> {
+ OutboxMessage managed = outboxMessageRepository.findById(message.getId());
+ if (managed != null) {
+ managed.markAsFailed();
+ logger.errorf("Outbox message %s has exceeded max retries, marking as FAILED", message.getId());
+ }
+ });
+ }
+}
diff --git a/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessageRepository.java b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessageRepository.java
new file mode 100644
index 0000000..0a00257
--- /dev/null
+++ b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxMessageRepository.java
@@ -0,0 +1,18 @@
+package dev.matheuscruz.infra.outbox;
+
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.List;
+import java.util.UUID;
+
+@ApplicationScoped
+public class OutboxMessageRepository implements PanacheRepositoryBase {
+
+ private static final int MAX_RETRIES = 10;
+ private static final int BATCH_SIZE = 20;
+
+ public List findPendingMessages() {
+ return find("status = ?1 and retryCount < ?2 order by createdAt", OutboxStatus.PENDING, MAX_RETRIES)
+ .range(0, BATCH_SIZE - 1).list();
+ }
+}
diff --git a/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxStatus.java b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxStatus.java
new file mode 100644
index 0000000..d3e8be8
--- /dev/null
+++ b/timeless-api/src/main/java/dev/matheuscruz/infra/outbox/OutboxStatus.java
@@ -0,0 +1,5 @@
+package dev.matheuscruz.infra.outbox;
+
+public enum OutboxStatus {
+ PENDING, SENT, FAILED
+}
diff --git a/timeless-api/src/main/java/dev/matheuscruz/infra/queue/SQS.java b/timeless-api/src/main/java/dev/matheuscruz/infra/queue/SQS.java
index 6aae662..a815d9a 100644
--- a/timeless-api/src/main/java/dev/matheuscruz/infra/queue/SQS.java
+++ b/timeless-api/src/main/java/dev/matheuscruz/infra/queue/SQS.java
@@ -13,122 +13,126 @@
import dev.matheuscruz.infra.ai.data.RecognizedOperation;
import dev.matheuscruz.infra.ai.data.RecognizedTransaction;
import dev.matheuscruz.infra.ai.data.SimpleMessage;
+import dev.matheuscruz.infra.outbox.OutboxMessage;
+import dev.matheuscruz.infra.outbox.OutboxMessageRepository;
import io.quarkus.narayana.jta.QuarkusTransaction;
-import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
-import java.io.IOException;
import java.util.Optional;
-import java.util.UUID;
+import java.util.concurrent.CompletionStage;
import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.eclipse.microprofile.reactive.messaging.Message;
import org.jboss.logging.Logger;
-import software.amazon.awssdk.services.sqs.SqsClient;
@ApplicationScoped
public class SQS {
- final String incomingMessagesUrl;
- final String processedMessagesUrl;
- final SqsClient sqs;
final ObjectMapper objectMapper;
final TextAiService aiService;
final RecordRepository recordRepository;
final UserRepository userRepository;
+ final OutboxMessageRepository outboxMessageRepository;
+ final String recognizedQueueUrl;
final Logger logger = Logger.getLogger(SQS.class);
private static final ObjectReader INCOMING_MESSAGE_READER = new ObjectMapper().readerFor(IncomingMessage.class);
- private static final ObjectReader AI_RESPONSE_READER = new ObjectMapper().readerFor(RecognizedOperation.class);
+ public SQS(ObjectMapper objectMapper, TextAiService aiService, RecordRepository recordRepository,
+ UserRepository userRepository, OutboxMessageRepository outboxMessageRepository,
+ @ConfigProperty(name = "whatsapp.recognized-message.queue-url") String recognizedQueueUrl) {
- public SQS(SqsClient sqs, @ConfigProperty(name = "whatsapp.incoming-message.queue-url") String incomingMessagesUrl,
- @ConfigProperty(name = "whatsapp.recognized-message.queue-url") String messagesProcessedUrl,
- ObjectMapper objectMapper, TextAiService aiService, RecordRepository recordRepository,
- UserRepository userRepository) {
-
- this.sqs = sqs;
- this.incomingMessagesUrl = incomingMessagesUrl;
- this.processedMessagesUrl = messagesProcessedUrl;
this.objectMapper = objectMapper;
this.aiService = aiService;
this.recordRepository = recordRepository;
this.userRepository = userRepository;
+ this.outboxMessageRepository = outboxMessageRepository;
+ this.recognizedQueueUrl = recognizedQueueUrl;
}
- @Scheduled(every = "5s")
- public void receiveMessages() {
- sqs.receiveMessage(req -> req.maxNumberOfMessages(10).queueUrl(incomingMessagesUrl)).messages()
- .forEach(message -> processMessage(message.body(), message.receiptHandle()));
- }
-
- private void processMessage(String body, String receiptHandle) {
+ @Incoming("whatsapp-incoming")
+ public CompletionStage receiveMessages(Message message) {
+ String body = message.getPayload();
IncomingMessage incomingMessage = parseIncomingMessage(body);
- if (!MessageKind.TEXT.equals(incomingMessage.kind()))
- return;
+
+ if (!MessageKind.TEXT.equals(incomingMessage.kind())) {
+ return message.ack();
+ }
Optional user = this.userRepository.findByPhoneNumber(incomingMessage.sender());
if (user.isEmpty()) {
- logger.error("User not found. Deleting message from queue.");
- deleteMessageUsing(receiptHandle);
- return;
+ logger.error("User not found.");
+ return message.nack(new RuntimeException("User not found for phone: " + incomingMessage.sender()));
}
- handleUserMessage(user.get(), incomingMessage, receiptHandle);
+ try {
+ handleUserMessage(user.get(), incomingMessage);
+ return message.ack();
+ } catch (Exception e) {
+ logger.error("Failed to process message: " + incomingMessage.messageId(), e);
+ return message.nack(e);
+ }
}
- private void handleUserMessage(User user, IncomingMessage message, String receiptHandle) {
- try {
- AllRecognizedOperations allRecognizedOperations = aiService.handleMessage(message.messageBody(),
- user.getId());
-
- for (RecognizedOperation recognizedOperation : allRecognizedOperations.all()) {
- switch (recognizedOperation.operation()) {
- case AiOperations.ADD_TRANSACTION ->
- processAddTransactionMessage(user, message, receiptHandle, recognizedOperation);
- case AiOperations.GET_BALANCE -> {
- logger.info("Processing GET_BALANCE operation" + recognizedOperation.recognizedTransaction());
- processSimpleMessage(user, message, receiptHandle, recognizedOperation);
- }
- default -> logger.warnf("Unknown operation type: %s", recognizedOperation.operation());
+ private void handleUserMessage(User user, IncomingMessage message) {
+ AllRecognizedOperations allRecognizedOperations = aiService.handleMessage(message.messageBody(), user.getId());
+
+ for (RecognizedOperation recognizedOperation : allRecognizedOperations.all()) {
+ switch (recognizedOperation.operation()) {
+ case AiOperations.ADD_TRANSACTION -> processAddTransactionMessage(user, message, recognizedOperation);
+ case AiOperations.GET_BALANCE -> {
+ logger.info("Processing GET_BALANCE operation" + recognizedOperation.recognizedTransaction());
+ processSimpleMessage(user, message, recognizedOperation);
}
+ default -> logger.warnf("Unknown operation type: %s", recognizedOperation.operation());
}
-
- } catch (Exception e) {
- logger.error("Failed to process message: " + message.messageId(), e);
}
}
- private void processAddTransactionMessage(User user, IncomingMessage message, String receiptHandle,
- RecognizedOperation recognizedOperation) throws IOException {
+ private void processAddTransactionMessage(User user, IncomingMessage message,
+ RecognizedOperation recognizedOperation) {
RecognizedTransaction recognizedTransaction = recognizedOperation.recognizedTransaction();
- sendProcessedMessage(new TransactionMessageProcessed(AiOperations.ADD_TRANSACTION.commandName(),
- message.messageId(), MessageStatus.PROCESSED, user.getPhoneNumber(), recognizedTransaction.withError(),
- recognizedTransaction));
Record record = new Record.Builder().userId(user.getId()).amount(recognizedTransaction.amount())
.description(recognizedTransaction.description()).transaction(recognizedTransaction.type())
.category(recognizedTransaction.category()).build();
- QuarkusTransaction.requiringNew().run(() -> recordRepository.persist(record));
+ TransactionMessageProcessed processed = new TransactionMessageProcessed(
+ AiOperations.ADD_TRANSACTION.commandName(), message.messageId(), MessageStatus.PROCESSED,
+ user.getPhoneNumber(), recognizedTransaction.withError(), recognizedTransaction);
+
+ OutboxMessage outboxMessage = new OutboxMessage(serialize(processed), recognizedQueueUrl,
+ user.getPhoneNumber());
- deleteMessageUsing(receiptHandle);
+ QuarkusTransaction.requiringNew().run(() -> {
+ recordRepository.persist(record);
+ outboxMessageRepository.persist(outboxMessage);
+ });
logger.infof("Message %s processed as ADD_TRANSACTION", message.messageId());
}
- private void processSimpleMessage(User user, IncomingMessage message, String receiptHandle,
- RecognizedOperation recognizedOperation) throws IOException {
+ private void processSimpleMessage(User user, IncomingMessage message, RecognizedOperation recognizedOperation) {
logger.infof("Processing simple message for user %s", recognizedOperation.recognizedTransaction());
SimpleMessage response = new SimpleMessage(recognizedOperation.recognizedTransaction().description());
- sendProcessedMessage(new SimpleMessageProcessed(AiOperations.GET_BALANCE.commandName(), message.messageId(),
- MessageStatus.PROCESSED, user.getPhoneNumber(), response));
- deleteMessageUsing(receiptHandle);
+
+ SimpleMessageProcessed processed = new SimpleMessageProcessed(AiOperations.GET_BALANCE.commandName(),
+ message.messageId(), MessageStatus.PROCESSED, user.getPhoneNumber(), response);
+
+ OutboxMessage outboxMessage = new OutboxMessage(serialize(processed), recognizedQueueUrl,
+ user.getPhoneNumber());
+
+ QuarkusTransaction.requiringNew().run(() -> outboxMessageRepository.persist(outboxMessage));
+
logger.infof("Message %s processed as GET_BALANCE", message.messageId());
}
- private void sendProcessedMessage(Object processedMessage) throws JsonProcessingException {
- String messageBody = objectMapper.writeValueAsString(processedMessage);
- sqs.sendMessage(req -> req.messageBody(messageBody).messageGroupId("ProcessedMessages")
- .messageDeduplicationId(UUID.randomUUID().toString()).queueUrl(processedMessagesUrl));
+ private String serialize(Object message) {
+ try {
+ return objectMapper.writeValueAsString(message);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to serialize message", e);
+ }
}
private IncomingMessage parseIncomingMessage(String messageBody) {
@@ -139,10 +143,6 @@ private IncomingMessage parseIncomingMessage(String messageBody) {
}
}
- private void deleteMessageUsing(String receiptHandle) {
- sqs.deleteMessage(req -> req.queueUrl(incomingMessagesUrl).receiptHandle(receiptHandle));
- }
-
public record TransactionMessageProcessed(String kind, String messageId, MessageStatus status, String user,
Boolean withError, RecognizedTransaction content) {
}
diff --git a/timeless-api/src/main/resources/application.properties b/timeless-api/src/main/resources/application.properties
index 219f81e..b1c1a0d 100644
--- a/timeless-api/src/main/resources/application.properties
+++ b/timeless-api/src/main/resources/application.properties
@@ -5,6 +5,11 @@ security.sensible.secret=${SECURITY_KEY}
whatsapp.incoming-message.queue-url=${INCOMING_MESSAGE_FIFO_URL}
whatsapp.recognized-message.queue-url=${RECOGNIZED_MESSAGE_FIFO_URL}
+# smallrye reactive messaging sqs
+mp.messaging.incoming.whatsapp-incoming.connector=smallrye-sqs
+mp.messaging.incoming.whatsapp-incoming.queue=${INCOMING_MESSAGE_FIFO_URL}
+mp.messaging.incoming.whatsapp-incoming.visibility-timeout=30
+
# aws sqs
quarkus.sqs.devservices.enabled=false
diff --git a/timeless-api/src/main/webui/package-lock.json b/timeless-api/src/main/webui/package-lock.json
index d9dc410..53c8807 100644
--- a/timeless-api/src/main/webui/package-lock.json
+++ b/timeless-api/src/main/webui/package-lock.json
@@ -348,7 +348,6 @@
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.12.tgz",
"integrity": "sha512-91mgQI15qStL38LijoKyAvNo61wB5rUpwqDVHoJQeISUChVYOY4hiofO6hW6ERg8MHQKUTyOrPDg5cN4yTcp9A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -516,7 +515,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.12.tgz",
"integrity": "sha512-b7IRSM9fWPmZ1SLN0utVcW87IkhiRte3Wsnwr2nEsjum2soRMfvKqHwtEFGfCztlwOmZLgKiGW9pqKpzBkIjnQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -533,7 +531,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.12.tgz",
"integrity": "sha512-246iBwMAVGzrYPqu/Wwzb9L/kt+dkT12Hllr/dYZu6aHeIxaHPRZoPBKSweAgOPXeOl+q+nlPtK34glsMb1CRw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -547,7 +544,6 @@
"integrity": "sha512-YQ15Yp2OWBS1NnzZH77HLH1ZDn+/A5Mc1EobKl4CX8dYUEPIB/KwmGKLaKtbJ0KNcVsDlmsTTWodRgqe2n5erw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "7.29.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -580,7 +576,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.12.tgz",
"integrity": "sha512-wcD6tzE30nwg58KmAU19347Jf/1F/vFg2CEd9Qcu5cA1Z4s3umzvaqs/7988ne4HaS4iJEpvTbRvGss7EYZEfA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -606,7 +601,6 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.12.tgz",
"integrity": "sha512-jhHaIgMWcgPcVFEPwhjLhByvA2xou6Th5PR6iC3H0YeLQyRmOFPWdczszytlWB1CeJ0UT9epxzOZT25zNcGSfg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"tslib": "^2.3.0"
@@ -626,7 +620,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.12.tgz",
"integrity": "sha512-P4MVColcYgBPmHyQ9nPVw9NjWPNxkC++N2Bjh3kOUFflC/6D/ufYJytsI/y1WQ8dtoHPHxiuRf3xHvcwUMPgEQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -667,7 +660,6 @@
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.12.tgz",
"integrity": "sha512-2/RDHt3GdW2ABNRVrgLX7IxgJLdF7u8Sbh11kAUn04QhNI/GObxIV4M5Hm/NTeDoi+hCXavkaHVBlj/dG5ANbw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -712,7 +704,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -985,6 +976,31 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -992,6 +1008,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1705,7 +1722,6 @@
"integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@inquirer/checkbox": "^4.3.2",
"@inquirer/confirm": "^5.1.21",
@@ -3942,7 +3958,6 @@
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.19.0"
}
@@ -4333,7 +4348,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -4541,7 +4555,6 @@
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
@@ -5409,7 +5422,6 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -5896,7 +5908,6 @@
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -6391,15 +6402,13 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.13.0.tgz",
"integrity": "sha512-vsYjfh7lyqvZX5QgqKc4YH8phs7g96Z8bsdIFNEU3VqXhlHaq+vov/Fgn/sr6MiUczdZkyXRC3TX369Ll4Nzbw==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
- "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -6504,7 +6513,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@colors/colors": "1.5.0",
"body-parser": "^1.19.0",
@@ -6987,7 +6995,6 @@
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cli-truncate": "^5.0.0",
"colorette": "^2.0.20",
@@ -8230,7 +8237,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -8808,7 +8814,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -9436,7 +9441,6 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -9644,8 +9648,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/tuf-js": {
"version": "4.1.0",
@@ -9683,7 +9686,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9829,7 +9831,6 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -10157,7 +10158,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -10176,8 +10176,7 @@
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
}
}
}