@solidstarters/solid-core 1.2.146 → 1.2.149

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.
Files changed (156) hide show
  1. package/dist/config/common.config.d.ts +10 -0
  2. package/dist/config/common.config.d.ts.map +1 -1
  3. package/dist/config/common.config.js +5 -0
  4. package/dist/config/common.config.js.map +1 -1
  5. package/dist/controllers/ai-interaction.controller.d.ts +1 -1
  6. package/dist/controllers/ai-interaction.controller.js +1 -1
  7. package/dist/dtos/create-ai-interaction.dto.d.ts +3 -0
  8. package/dist/dtos/create-ai-interaction.dto.d.ts.map +1 -1
  9. package/dist/dtos/create-ai-interaction.dto.js +20 -1
  10. package/dist/dtos/create-ai-interaction.dto.js.map +1 -1
  11. package/dist/dtos/update-ai-interaction.dto.d.ts +3 -0
  12. package/dist/dtos/update-ai-interaction.dto.d.ts.map +1 -1
  13. package/dist/dtos/update-ai-interaction.dto.js +19 -1
  14. package/dist/dtos/update-ai-interaction.dto.js.map +1 -1
  15. package/dist/entities/ai-interaction.entity.d.ts +3 -0
  16. package/dist/entities/ai-interaction.entity.d.ts.map +1 -1
  17. package/dist/entities/ai-interaction.entity.js +17 -1
  18. package/dist/entities/ai-interaction.entity.js.map +1 -1
  19. package/dist/helpers/environment.helper.d.ts +2 -0
  20. package/dist/helpers/environment.helper.d.ts.map +1 -0
  21. package/dist/helpers/environment.helper.js +11 -0
  22. package/dist/helpers/environment.helper.js.map +1 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/interfaces.d.ts +5 -5
  28. package/dist/interfaces.d.ts.map +1 -1
  29. package/dist/interfaces.js.map +1 -1
  30. package/dist/jobs/database/api-email-subscriber-database.service.d.ts +3 -1
  31. package/dist/jobs/database/api-email-subscriber-database.service.d.ts.map +1 -1
  32. package/dist/jobs/database/api-email-subscriber-database.service.js +6 -3
  33. package/dist/jobs/database/api-email-subscriber-database.service.js.map +1 -1
  34. package/dist/jobs/database/computed-field-evaluation-subscriber.service.d.ts +3 -1
  35. package/dist/jobs/database/computed-field-evaluation-subscriber.service.d.ts.map +1 -1
  36. package/dist/jobs/database/computed-field-evaluation-subscriber.service.js +6 -3
  37. package/dist/jobs/database/computed-field-evaluation-subscriber.service.js.map +1 -1
  38. package/dist/jobs/database/generate-code-subscriber-database.service.d.ts +3 -1
  39. package/dist/jobs/database/generate-code-subscriber-database.service.d.ts.map +1 -1
  40. package/dist/jobs/database/generate-code-subscriber-database.service.js +6 -3
  41. package/dist/jobs/database/generate-code-subscriber-database.service.js.map +1 -1
  42. package/dist/jobs/database/otp-subscriber-database.service.d.ts +3 -1
  43. package/dist/jobs/database/otp-subscriber-database.service.d.ts.map +1 -1
  44. package/dist/jobs/database/otp-subscriber-database.service.js +5 -2
  45. package/dist/jobs/database/otp-subscriber-database.service.js.map +1 -1
  46. package/dist/jobs/database/sms-subscriber-database.service.d.ts +4 -2
  47. package/dist/jobs/database/sms-subscriber-database.service.d.ts.map +1 -1
  48. package/dist/jobs/database/sms-subscriber-database.service.js +7 -4
  49. package/dist/jobs/database/sms-subscriber-database.service.js.map +1 -1
  50. package/dist/jobs/database/smtp-email-subscriber-database.service.d.ts +4 -2
  51. package/dist/jobs/database/smtp-email-subscriber-database.service.d.ts.map +1 -1
  52. package/dist/jobs/database/smtp-email-subscriber-database.service.js +6 -3
  53. package/dist/jobs/database/smtp-email-subscriber-database.service.js.map +1 -1
  54. package/dist/jobs/database/test-queue-subscriber-database.service.d.ts +3 -1
  55. package/dist/jobs/database/test-queue-subscriber-database.service.d.ts.map +1 -1
  56. package/dist/jobs/database/test-queue-subscriber-database.service.js +6 -3
  57. package/dist/jobs/database/test-queue-subscriber-database.service.js.map +1 -1
  58. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.d.ts +3 -1
  59. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.d.ts.map +1 -1
  60. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.js +7 -2
  61. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.js.map +1 -1
  62. package/dist/jobs/database/twilio-sms-publisher-database.service.d.ts +11 -0
  63. package/dist/jobs/database/twilio-sms-publisher-database.service.d.ts.map +1 -0
  64. package/dist/jobs/database/twilio-sms-publisher-database.service.js +39 -0
  65. package/dist/jobs/database/twilio-sms-publisher-database.service.js.map +1 -0
  66. package/dist/jobs/database/twilio-sms-queue-database-options.d.ts +8 -0
  67. package/dist/jobs/database/twilio-sms-queue-database-options.d.ts.map +1 -0
  68. package/dist/jobs/database/twilio-sms-queue-database-options.js +10 -0
  69. package/dist/jobs/database/twilio-sms-queue-database-options.js.map +1 -0
  70. package/dist/jobs/database/twilio-sms-subscriber-database.service.d.ts +17 -0
  71. package/dist/jobs/database/twilio-sms-subscriber-database.service.d.ts.map +1 -0
  72. package/dist/jobs/database/twilio-sms-subscriber-database.service.js +48 -0
  73. package/dist/jobs/database/twilio-sms-subscriber-database.service.js.map +1 -0
  74. package/dist/jobs/database/whatsapp-subscriber-database.service.d.ts +3 -1
  75. package/dist/jobs/database/whatsapp-subscriber-database.service.d.ts.map +1 -1
  76. package/dist/jobs/database/whatsapp-subscriber-database.service.js +6 -3
  77. package/dist/jobs/database/whatsapp-subscriber-database.service.js.map +1 -1
  78. package/dist/jobs/sms-subscriber.service.d.ts +1 -1
  79. package/dist/jobs/sms-subscriber.service.js +1 -1
  80. package/dist/jobs/sms-subscriber.service.js.map +1 -1
  81. package/dist/jobs/smtp-email-subscriber.service.d.ts +1 -1
  82. package/dist/seeders/module-metadata-seeder.service.d.ts +1 -1
  83. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  84. package/dist/seeders/module-metadata-seeder.service.js +15 -4
  85. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  86. package/dist/seeders/seed-data/solid-core-metadata.json +83 -3
  87. package/dist/services/ai-interaction.service.d.ts +1 -1
  88. package/dist/services/ai-interaction.service.d.ts.map +1 -1
  89. package/dist/services/ai-interaction.service.js +5 -1
  90. package/dist/services/ai-interaction.service.js.map +1 -1
  91. package/dist/services/mail/smtp-email.service.d.ts +4 -4
  92. package/dist/services/mail/smtp-email.service.d.ts.map +1 -1
  93. package/dist/services/mail/smtp-email.service.js +12 -8
  94. package/dist/services/mail/smtp-email.service.js.map +1 -1
  95. package/dist/services/mq-message.service.d.ts +9 -0
  96. package/dist/services/mq-message.service.d.ts.map +1 -1
  97. package/dist/services/mq-message.service.js +61 -0
  98. package/dist/services/mq-message.service.js.map +1 -1
  99. package/dist/services/poller.service.d.ts +24 -0
  100. package/dist/services/poller.service.d.ts.map +1 -0
  101. package/dist/services/poller.service.js +131 -0
  102. package/dist/services/poller.service.js.map +1 -0
  103. package/dist/services/queues/database-subscriber.service.d.ts +4 -1
  104. package/dist/services/queues/database-subscriber.service.d.ts.map +1 -1
  105. package/dist/services/queues/database-subscriber.service.js +13 -13
  106. package/dist/services/queues/database-subscriber.service.js.map +1 -1
  107. package/dist/services/sms/Msg91BaseSMSService.d.ts +2 -2
  108. package/dist/services/sms/Msg91BaseSMSService.d.ts.map +1 -1
  109. package/dist/services/sms/Msg91BaseSMSService.js.map +1 -1
  110. package/dist/services/sms/Msg91OTPService.d.ts +1 -1
  111. package/dist/services/sms/Msg91OTPService.d.ts.map +1 -1
  112. package/dist/services/sms/Msg91OTPService.js.map +1 -1
  113. package/dist/services/sms/Msg91SMSService.d.ts +1 -1
  114. package/dist/services/sms/Msg91SMSService.d.ts.map +1 -1
  115. package/dist/services/sms/Msg91SMSService.js.map +1 -1
  116. package/dist/services/sms/TwilioSMSService.d.ts +18 -0
  117. package/dist/services/sms/TwilioSMSService.d.ts.map +1 -0
  118. package/dist/services/sms/TwilioSMSService.js +115 -0
  119. package/dist/services/sms/TwilioSMSService.js.map +1 -0
  120. package/dist/solid-core.module.d.ts.map +1 -1
  121. package/dist/solid-core.module.js +11 -0
  122. package/dist/solid-core.module.js.map +1 -1
  123. package/dist/tsconfig.tsbuildinfo +1 -1
  124. package/package.json +4 -3
  125. package/src/config/common.config.ts +5 -0
  126. package/src/dtos/create-ai-interaction.dto.ts +16 -5
  127. package/src/dtos/update-ai-interaction.dto.ts +16 -5
  128. package/src/entities/ai-interaction.entity.ts +11 -3
  129. package/src/helpers/environment.helper.ts +7 -0
  130. package/src/index.ts +5 -0
  131. package/src/interfaces.ts +5 -5
  132. package/src/jobs/database/api-email-subscriber-database.service.ts +3 -1
  133. package/src/jobs/database/computed-field-evaluation-subscriber.service.ts +4 -2
  134. package/src/jobs/database/generate-code-subscriber-database.service.ts +3 -1
  135. package/src/jobs/database/otp-subscriber-database.service.ts +3 -1
  136. package/src/jobs/database/sms-subscriber-database.service.ts +4 -2
  137. package/src/jobs/database/smtp-email-subscriber-database.service.ts +3 -1
  138. package/src/jobs/database/test-queue-subscriber-database.service.ts +3 -1
  139. package/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts +5 -1
  140. package/src/jobs/database/twilio-sms-publisher-database.service.ts +23 -0
  141. package/src/jobs/database/twilio-sms-queue-database-options.ts +9 -0
  142. package/src/jobs/database/twilio-sms-subscriber-database.service.ts +32 -0
  143. package/src/jobs/database/whatsapp-subscriber-database.service.ts +3 -1
  144. package/src/jobs/sms-subscriber.service.ts +1 -1
  145. package/src/seeders/module-metadata-seeder.service.ts +18 -15
  146. package/src/seeders/seed-data/solid-core-metadata.json +83 -3
  147. package/src/services/ai-interaction.service.ts +8 -3
  148. package/src/services/mail/smtp-email.service.ts +18 -17
  149. package/src/services/mq-message.service.ts +116 -0
  150. package/src/services/poller.service.ts +163 -0
  151. package/src/services/queues/database-subscriber.service.ts +39 -12
  152. package/src/services/sms/Msg91BaseSMSService.ts +2 -2
  153. package/src/services/sms/Msg91OTPService.ts +1 -1
  154. package/src/services/sms/Msg91SMSService.ts +1 -1
  155. package/src/services/sms/TwilioSMSService.ts +118 -0
  156. package/src/solid-core.module.ts +14 -0
@@ -4919,7 +4919,7 @@
4919
4919
  "tableName": "ss_ai_interactions",
4920
4920
  "dataSource": "default",
4921
4921
  "dataSourceType": "postgres",
4922
- "userKeyFieldUserKey": "message",
4922
+ "userKeyFieldUserKey": "externalId",
4923
4923
  "isSystem": false,
4924
4924
  "fields": [
4925
4925
  {
@@ -4938,6 +4938,35 @@
4938
4938
  "relationModelModuleName": "solid-core",
4939
4939
  "isSystem": true
4940
4940
  },
4941
+ {
4942
+ "name": "externalId",
4943
+ "displayName": "External ID",
4944
+ "description": "Used to track using a reference number of each ai interaction.",
4945
+ "type": "computed",
4946
+ "ormType": "varchar",
4947
+ "isSystem": false,
4948
+ "computedFieldValueType": "string",
4949
+ "computedFieldTriggerConfig": [
4950
+ {
4951
+ "modelName": "aiInteraction",
4952
+ "moduleName": "solid-core",
4953
+ "operations": [
4954
+ "before-insert"
4955
+ ]
4956
+ }
4957
+ ],
4958
+ "computedFieldValueProvider": "AlphaNumExternalIdComputationProvider",
4959
+ "computedFieldValueProviderCtxt": "{\n \"prefix\": \"AI\",\n \"length\": \"10\"\n}",
4960
+ "required": true,
4961
+ "unique": true,
4962
+ "index": true,
4963
+ "private": false,
4964
+ "encrypt": false,
4965
+ "encryptionType": null,
4966
+ "decryptWhen": null,
4967
+ "columnName": null,
4968
+ "isUserKey": true
4969
+ },
4941
4970
  {
4942
4971
  "name": "threadId",
4943
4972
  "displayName": "Thread ID",
@@ -4950,6 +4979,22 @@
4950
4979
  "private": false,
4951
4980
  "encrypt": false
4952
4981
  },
4982
+ {
4983
+ "name": "parentInteraction",
4984
+ "displayName": "Parent Interaction",
4985
+ "type": "relation",
4986
+ "required": false,
4987
+ "unique": false,
4988
+ "index": true,
4989
+ "private": false,
4990
+ "encrypt": false,
4991
+ "relationType": "many-to-one",
4992
+ "relationCoModelSingularName": "aiInteraction",
4993
+ "relationCreateInverse": false,
4994
+ "relationCascade": "set null",
4995
+ "relationModelModuleName": "solid-core",
4996
+ "isSystem": true
4997
+ },
4953
4998
  {
4954
4999
  "name": "role",
4955
5000
  "displayName": "Role",
@@ -5057,6 +5102,17 @@
5057
5102
  "index": false,
5058
5103
  "private": false,
5059
5104
  "encrypt": false
5105
+ },
5106
+ {
5107
+ "name": "isAutoApply",
5108
+ "displayName": "Is Auto Apply",
5109
+ "type": "boolean",
5110
+ "ormType": "boolean",
5111
+ "required": false,
5112
+ "unique": false,
5113
+ "index": false,
5114
+ "private": false,
5115
+ "encrypt": false
5060
5116
  }
5061
5117
  ]
5062
5118
  }
@@ -11867,7 +11923,7 @@
11867
11923
  }
11868
11924
  },
11869
11925
  {
11870
- "name": "ai-interaction-list-view",
11926
+ "name": "aiInteraction-list-view",
11871
11927
  "displayName": "AI Interaction",
11872
11928
  "type": "list",
11873
11929
  "context": "{}",
@@ -11900,6 +11956,18 @@
11900
11956
  "name": "threadId"
11901
11957
  }
11902
11958
  },
11959
+ {
11960
+ "type": "field",
11961
+ "attrs": {
11962
+ "name": "externalId"
11963
+ }
11964
+ },
11965
+ {
11966
+ "type": "field",
11967
+ "attrs": {
11968
+ "name": "parentInteraction"
11969
+ }
11970
+ },
11903
11971
  {
11904
11972
  "type": "field",
11905
11973
  "attrs": {
@@ -11934,7 +12002,7 @@
11934
12002
  }
11935
12003
  },
11936
12004
  {
11937
- "name": "ai-interaction-form-view",
12005
+ "name": "aiInteraction-form-view",
11938
12006
  "displayName": "AI Interaction",
11939
12007
  "type": "form",
11940
12008
  "context": "{}",
@@ -11987,6 +12055,12 @@
11987
12055
  "name": "user"
11988
12056
  }
11989
12057
  },
12058
+ {
12059
+ "type": "field",
12060
+ "attrs": {
12061
+ "name": "externalId"
12062
+ }
12063
+ },
11990
12064
  {
11991
12065
  "type": "field",
11992
12066
  "attrs": {
@@ -12034,6 +12108,12 @@
12034
12108
  "attrs": {
12035
12109
  "name": "responseTimeMs"
12036
12110
  }
12111
+ },
12112
+ {
12113
+ "type": "field",
12114
+ "attrs": {
12115
+ "name": "parentInteraction"
12116
+ }
12037
12117
  }
12038
12118
  ]
12039
12119
  }
@@ -43,7 +43,7 @@ export class AiInteractionService extends CRUDService<AiInteraction> {
43
43
  super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService, entityManager, repo, 'aiInteraction', 'solid-core', moduleRef);
44
44
  }
45
45
 
46
- async triggerMcpClientJob(prompt: string): Promise<string> {
46
+ async triggerMcpClientJob(prompt: string): Promise<any> {
47
47
  const activeUser: ActiveUserData = this.requestContextService.getActiveUser();
48
48
 
49
49
  const aiInteraction = await this.create({
@@ -65,7 +65,12 @@ export class AiInteractionService extends CRUDService<AiInteraction> {
65
65
  parentEntityId: aiInteraction.id,
66
66
  };
67
67
 
68
- return await this.publisherFactory.publish(m, 'TriggerMcpClientPublisher');
68
+ const queueMessageId = await this.publisherFactory.publish(m, 'TriggerMcpClientPublisher');
69
+
70
+ return {
71
+ queueMessageId: queueMessageId,
72
+ aiInteractionId: aiInteraction.id
73
+ }
69
74
  }
70
75
 
71
76
  /**
@@ -206,7 +211,7 @@ export class AiInteractionService extends CRUDService<AiInteraction> {
206
211
 
207
212
  // TODO: This provider to implement an interface - IMcpToolResponseHandler ... apply(aiInteraction: AiInteraction)
208
213
  // throw new Error('Method not implemented.');
209
-
214
+
210
215
  // Mark the interaction as applied
211
216
  await this.update(aiInteraction.id, { isApplied: true }, [], true);
212
217
 
@@ -47,7 +47,7 @@ export class SMTPEMailService implements IMail {
47
47
  cc?: string[],
48
48
  bcc?: string[],
49
49
  from?: string
50
- ): Promise<void> {
50
+ ) {
51
51
  // Load template and evaluate it.
52
52
  const emailTemplate = await this.emailTemplateService.findOneByName(templateName);
53
53
  if (!emailTemplate) {
@@ -63,7 +63,7 @@ export class SMTPEMailService implements IMail {
63
63
  const subject = subjectTemplate(templateParams);
64
64
 
65
65
  // Finally send the email.
66
- await this.sendEmail(to, subject, body, shouldQueueEmails, wrapperAttachments, attachments, parentEntity, parentEntityId, cc, bcc, from);
66
+ return await this.sendEmail(to, subject, body, shouldQueueEmails, wrapperAttachments, attachments, parentEntity, parentEntityId, cc, bcc, from);
67
67
  }
68
68
 
69
69
  async sendEmail(
@@ -78,7 +78,7 @@ export class SMTPEMailService implements IMail {
78
78
  cc?: string[],
79
79
  bcc?: string[],
80
80
  from?: string
81
- ): Promise<void> {
81
+ ) {
82
82
  const message = {
83
83
  payload: {
84
84
  from: from || this.commonConfiguration.smtpMail.from,
@@ -95,33 +95,34 @@ export class SMTPEMailService implements IMail {
95
95
 
96
96
  // Send using queue if the developer has explicitly invoked with true.
97
97
  if (shouldQueueEmails === true) {
98
- this.sendEmailAsynchronously(message);
98
+ return this.sendEmailAsynchronously(message);
99
99
  }
100
100
  // If developer has not, however system config mandates that we send using queue, still we send.
101
101
  else if (shouldQueueEmails == false && this.commonConfiguration.shouldQueueEmails === true) {
102
- this.sendEmailAsynchronously(message);
102
+ return this.sendEmailAsynchronously(message);
103
103
  }
104
- // Else we send synch
104
+ // Else we send synchronously
105
105
  else {
106
- await this.sendEmailSynchronously(message);
106
+ return await this.sendEmailSynchronously(message);
107
107
  }
108
108
  }
109
109
 
110
110
  async sendEmailAsynchronously(message) {
111
111
  const { to, subject, body } = message.payload;
112
- // this.notificationPublisherService.publish(message);
113
- // this.emailPublisher.publish(message);
114
- // this.emailDbPublisher.publish(message);
115
-
116
- this.publisherFactory.publish(message, 'SmtpEmailQueuePublisher');
117
-
118
112
  this.logger.debug(`Queueing email to ${to} with subject ${subject} and body ${body}`);
113
+ return this.publisherFactory.publish(message, 'SmtpEmailQueuePublisher');
119
114
  }
120
115
 
121
- async sendEmailSynchronously(message: QueueMessage<any>): Promise<void> {
122
- const { from, to, subject, body, attachments, cc, bcc } = message.payload;
116
+ async sendEmailSynchronously(message: QueueMessage<any>) {
117
+ const { from, to, subject, body, attachments = [], cc, bcc } = message.payload;
123
118
 
124
- const attachmentsList = attachments.map((attachment: MailAttachment) => {
119
+ // if any of the required fields are missing, throw an error.
120
+ if (!from || !to || !subject || !body) {
121
+ this.logger.error(`Required fields are missing in the email message: ${JSON.stringify(message.payload)}`);
122
+ return;
123
+ }
124
+
125
+ const attachmentsList = attachments?.map((attachment: MailAttachment) => {
125
126
  const attachmentEntry = {
126
127
  filename: attachment.filename,
127
128
  contentType: attachment.contentType,
@@ -133,7 +134,7 @@ export class SMTPEMailService implements IMail {
133
134
  attachmentEntry['content'] = attachment.content;
134
135
  }
135
136
  return attachmentEntry;
136
- });
137
+ }) || [];
137
138
 
138
139
  // throw new Error('Random error....');
139
140
  const r = await this.transporter.sendMail({
@@ -68,4 +68,120 @@ export class MqMessageService extends CRUDService<MqMessage> {
68
68
  });
69
69
  }
70
70
 
71
+ /**
72
+ * Wait until a queue message reaches a terminal status (succeeded/failed).
73
+ *
74
+ * @param messageId string – the external message id you store in `ss_mq_message.messageId`
75
+ * @param opts.timeoutMs total time to wait before giving up (default 60s)
76
+ * @param opts.intervalMs initial poll interval (default 500ms)
77
+ * @param opts.maxIntervalMs cap for exponential backoff (default 2000ms)
78
+ * @param opts.throwOnFailure if true, throws when stage === 'failed' (default false)
79
+ * @param opts.parseJson try JSON.parse on `output` and `error` (default true)
80
+ * @returns resolves with the final MqMessage row when terminal, rejects on timeout (or failure if throwOnFailure)
81
+ */
82
+ async waitForTerminalStatus(
83
+ messageId: string,
84
+ opts?: {
85
+ timeoutMs?: number;
86
+ intervalMs?: number;
87
+ maxIntervalMs?: number;
88
+ throwOnFailure?: boolean;
89
+ parseJson?: boolean;
90
+ logEveryN?: number;
91
+ },
92
+ ): Promise<MqMessage> {
93
+ const {
94
+ timeoutMs = 60_000,
95
+ intervalMs = 500,
96
+ maxIntervalMs = 2_000,
97
+ throwOnFailure = false,
98
+ parseJson = true,
99
+ logEveryN = 10, // log every N polls to avoid noisy logs
100
+ } = opts || {};
101
+
102
+ const start = Date.now();
103
+ let attempt = 0;
104
+ let delay = intervalMs;
105
+
106
+ // Small helper
107
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
108
+
109
+ while (true) {
110
+ attempt++;
111
+
112
+ // Fetch minimal columns needed for quick polling
113
+ const rec = await this.repo.findOne({
114
+ where: { messageId },
115
+ select: {
116
+ id: true,
117
+ messageId: true,
118
+ stage: true,
119
+ finishedAt: true,
120
+ elapsedMillis: true,
121
+ output: true,
122
+ error: true,
123
+ input: true,
124
+ // add other fields if you need to return them
125
+ } as any,
126
+ loadEagerRelations: false,
127
+ });
128
+
129
+ if (attempt % logEveryN === 0) {
130
+ this.logger.debug(
131
+ `waitForTerminalStatus(${messageId}) poll #${attempt} -> ${rec?.stage ?? 'not_found'}`
132
+ );
133
+ }
134
+
135
+ if (!rec) {
136
+ // Not found yet – keep waiting until timeout
137
+ } else if (rec.stage === 'succeeded' || rec.stage === 'failed') {
138
+ // Optionally parse output/error if they contain JSON strings
139
+ if (parseJson) {
140
+ rec.output = this.safeJsonParse(rec.output);
141
+ rec.error = this.safeJsonParse(rec.error);
142
+ }
143
+
144
+ if (rec.stage === 'failed' && throwOnFailure) {
145
+ throw new Error(
146
+ `Queue message ${messageId} failed` +
147
+ (rec.error ? `: ${JSON.stringify(rec.error).slice(0, 500)}` : '')
148
+ );
149
+ }
150
+ return rec;
151
+ }
152
+
153
+ // Timeout?
154
+ const elapsed = Date.now() - start;
155
+ if (elapsed >= timeoutMs) {
156
+ throw new Error(`Timed out after ${timeoutMs}ms waiting for message ${messageId} to reach terminal status`);
157
+ }
158
+
159
+ // Backoff with cap
160
+ await sleep(delay);
161
+ delay = Math.min(Math.floor(delay * 1.5), maxIntervalMs);
162
+ }
163
+ }
164
+
165
+ // /**
166
+ // * Optional wrapper: publish and then wait (if your publisher returns the messageId).
167
+ // */
168
+ // async publishAndWait<T>(
169
+ // publishFn: () => Promise<string>, // returns messageId
170
+ // waitOpts?: Parameters<MqMessageService['waitForTerminalStatus']>[1],
171
+ // ): Promise<MqMessage> {
172
+ // const messageId = await publishFn();
173
+ // return this.waitForTerminalStatus(messageId, waitOpts);
174
+ // }
175
+
176
+ private safeJsonParse(value: unknown): unknown {
177
+ if (value == null) return value;
178
+ if (typeof value !== 'string') return value;
179
+ const s = value.trim();
180
+ if (!s) return s;
181
+ try {
182
+ return JSON.parse(s);
183
+ } catch {
184
+ return value; // leave as-is if not valid JSON
185
+ }
186
+ }
71
187
  }
@@ -0,0 +1,163 @@
1
+ // src/common/poller/poller.service.ts
2
+ import {
3
+ Injectable,
4
+ Logger,
5
+ OnModuleDestroy,
6
+ BeforeApplicationShutdown,
7
+ } from '@nestjs/common';
8
+
9
+ export interface PollOptions {
10
+ /** Wait after a successful iteration */
11
+ baseDelayMs?: number; // default 1000
12
+ /** Maximum delay after repeated failures */
13
+ maxDelayMs?: number; // default 30000
14
+ /** Per-iteration timeout guard */
15
+ timeoutPerIterationMs?: number; // default 60000
16
+ /** Add jitter to spread load */
17
+ jitter?: boolean; // default true
18
+ }
19
+
20
+ type ProcessNextFn = (queueName: string) => Promise<unknown>;
21
+
22
+ interface PollerState {
23
+ queueName: string;
24
+ processNext: ProcessNextFn;
25
+ opts: Required<PollOptions>;
26
+ inFlight: boolean;
27
+ stopped: boolean;
28
+ backoff: number;
29
+ nextTimer?: NodeJS.Timeout;
30
+ }
31
+
32
+ @Injectable()
33
+ export class PollerService implements OnModuleDestroy, BeforeApplicationShutdown {
34
+ private readonly logger = new Logger(PollerService.name);
35
+ private readonly pollers = new Map<string, PollerState>();
36
+
37
+ start(queueName: string, processNext: ProcessNextFn, options: PollOptions = {}): void {
38
+ if (this.pollers.has(queueName)) {
39
+ this.logger.warn(`Poller "${queueName}" already started; ignoring.`);
40
+ return;
41
+ }
42
+
43
+ const opts: Required<PollOptions> = {
44
+ baseDelayMs: options.baseDelayMs ?? 1000,
45
+ maxDelayMs: options.maxDelayMs ?? 30_000,
46
+ timeoutPerIterationMs: options.timeoutPerIterationMs ?? 5 * 60_000,
47
+ jitter: options.jitter ?? true,
48
+ };
49
+
50
+ const state: PollerState = {
51
+ queueName,
52
+ processNext,
53
+ opts,
54
+ inFlight: false,
55
+ stopped: false,
56
+ backoff: opts.baseDelayMs,
57
+ nextTimer: undefined,
58
+ };
59
+
60
+ this.pollers.set(queueName, state);
61
+ // kick off on next tick
62
+ setImmediate(() => this.poll(state).catch(() => { }));
63
+ this.logger.log(`Started poller "${queueName}"`);
64
+ }
65
+
66
+ stop(queueName: string): void {
67
+ const state = this.pollers.get(queueName);
68
+ if (!state) return;
69
+
70
+ state.stopped = true;
71
+ if (state.nextTimer) {
72
+ clearTimeout(state.nextTimer);
73
+ state.nextTimer = undefined;
74
+ }
75
+ this.pollers.delete(queueName);
76
+ this.logger.log(`Stopped poller "${queueName}"`);
77
+ }
78
+
79
+ stopAll(): void {
80
+ for (const name of Array.from(this.pollers.keys())) {
81
+ this.stop(name);
82
+ }
83
+ }
84
+
85
+ async onModuleDestroy(): Promise<void> {
86
+ this.stopAll();
87
+ }
88
+
89
+ async beforeApplicationShutdown(): Promise<void> {
90
+ this.stopAll();
91
+ }
92
+
93
+ // ---- internals ----
94
+
95
+ private async poll(state: PollerState): Promise<void> {
96
+ if (state.stopped || state.inFlight) return;
97
+ state.inFlight = true;
98
+
99
+ try {
100
+ await this.withTimeout(
101
+ state.processNext(state.queueName),
102
+ state.opts.timeoutPerIterationMs,
103
+ );
104
+
105
+ // success: reset backoff and schedule next run after base delay
106
+ state.backoff = state.opts.baseDelayMs;
107
+ // this.logger.debug(`[${state.queueName}] iteration completed`);
108
+ this.schedule(state, state.opts.baseDelayMs);
109
+ } catch (err: unknown) {
110
+ const msg = this.errorToString(err);
111
+ this.logger.error(`[${state.queueName}] iteration failed: ${msg}`);
112
+
113
+ // failure: schedule with backoff + optional jitter, then increase backoff
114
+ const wait = this.computeWait(state.backoff, state.opts);
115
+ state.backoff = Math.min(state.backoff * 2, state.opts.maxDelayMs);
116
+ this.schedule(state, wait);
117
+ } finally {
118
+ state.inFlight = false;
119
+ }
120
+ }
121
+
122
+ private schedule(state: PollerState, delayMs: number) {
123
+ if (state.stopped) return;
124
+ if (state.nextTimer) clearTimeout(state.nextTimer);
125
+
126
+ state.nextTimer = setTimeout(() => {
127
+ // clear reference before calling poll to avoid re-entrancy confusion
128
+ state.nextTimer = undefined;
129
+ this.poll(state).catch(() => { });
130
+ }, delayMs);
131
+ }
132
+
133
+ private computeWait(currentBackoff: number, opts: Required<PollOptions>): number {
134
+ if (!opts.jitter) return currentBackoff;
135
+ // Full jitter: random in [250ms, currentBackoff * 2], clamped to maxDelayMs
136
+ const doubled = Math.min(currentBackoff * 2, opts.maxDelayMs);
137
+ const jittered = Math.floor(Math.random() * doubled);
138
+ return Math.max(250, jittered);
139
+ }
140
+
141
+ private async withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
142
+ let timer: NodeJS.Timeout | undefined;
143
+ try {
144
+ return await Promise.race<T>([
145
+ p,
146
+ new Promise<never>((_, rej) => {
147
+ timer = setTimeout(() => rej(new Error(`Iteration timed out after ${ms} ms`)), ms);
148
+ }),
149
+ ]);
150
+ } finally {
151
+ if (timer) clearTimeout(timer);
152
+ }
153
+ }
154
+
155
+ private errorToString(err: unknown): string {
156
+ if (err instanceof Error) return err.stack ?? err.message;
157
+ try {
158
+ return JSON.stringify(err);
159
+ } catch {
160
+ return String(err);
161
+ }
162
+ }
163
+ }
@@ -3,6 +3,7 @@ import { QueuesModuleOptions } from "../../interfaces";
3
3
  import { QueueMessage, QueueSubscriber } from '../../interfaces/mq';
4
4
  import { MqMessageQueueService } from '../mq-message-queue.service';
5
5
  import { MqMessageService } from '../mq-message.service';
6
+ import { PollerService } from '../poller.service';
6
7
 
7
8
 
8
9
  export abstract class DatabaseSubscriber<T> implements OnModuleInit, QueueSubscriber<T> {
@@ -13,6 +14,7 @@ export abstract class DatabaseSubscriber<T> implements OnModuleInit, QueueSubscr
13
14
  constructor(
14
15
  protected readonly mqMessageService: MqMessageService,
15
16
  protected readonly mqMessageQueueService: MqMessageQueueService,
17
+ protected readonly poller: PollerService,
16
18
  ) {
17
19
  this.serviceRole = process.env.QUEUES_SERVICE_ROLE;
18
20
  if (!this.serviceRole) {
@@ -70,6 +72,31 @@ export abstract class DatabaseSubscriber<T> implements OnModuleInit, QueueSubscr
70
72
  // this.logger.debug(`#### DatabaseSubscriber finished processing message from queue: ${queueName}`);
71
73
  }
72
74
 
75
+ // async onModuleInit(): Promise<void> {
76
+ // // we will start subscriber only if the current service role is subscriber.
77
+ // if (['both', 'subscriber'].includes(this.serviceRole)) {
78
+
79
+ // const options = this.options();
80
+
81
+ // const queueName = options.queueName;
82
+ // // setInterval(() => this.processNext(queueName), 1000);
83
+ // const poll = async () => {
84
+ // try {
85
+ // await this.processNext(queueName);
86
+ // } catch (err) {
87
+ // this.logger.error(`Polling error: ${err.message}`);
88
+ // } finally {
89
+ // setTimeout(poll, 1000); // Wait 1s *after* processing finishes
90
+ // }
91
+ // };
92
+
93
+ // // start the loop
94
+ // poll();
95
+
96
+ // this.logger.log(`DatabaseSubscriber ready to consume messages: ${JSON.stringify(this.options())}`);
97
+ // }
98
+ // }
99
+
73
100
  async onModuleInit(): Promise<void> {
74
101
  // we will start subscriber only if the current service role is subscriber.
75
102
  if (['both', 'subscriber'].includes(this.serviceRole)) {
@@ -77,24 +104,24 @@ export abstract class DatabaseSubscriber<T> implements OnModuleInit, QueueSubscr
77
104
  const options = this.options();
78
105
 
79
106
  const queueName = options.queueName;
80
- // setInterval(() => this.processNext(queueName), 1000);
81
- const poll = async () => {
82
- try {
83
- await this.processNext(queueName);
84
- } catch (err) {
85
- this.logger.error(`Polling error: ${err.message}`);
86
- } finally {
87
- setTimeout(poll, 1000); // Wait 1s *after* processing finishes
88
- }
89
- };
90
107
 
91
- // start the loop
92
- poll();
108
+ this.poller.start(queueName, (q) => this.processNext(q), {
109
+ baseDelayMs: 1000,
110
+ maxDelayMs: 30_000,
111
+ timeoutPerIterationMs: 5 * 60_000,
112
+ jitter: true,
113
+ });
93
114
 
94
115
  this.logger.log(`DatabaseSubscriber ready to consume messages: ${JSON.stringify(this.options())}`);
95
116
  }
96
117
  }
97
118
 
119
+ onModuleDestroy() {
120
+ const options = this.options();
121
+ const queueName = options.queueName;
122
+ this.poller.stop(queueName);
123
+ }
124
+
98
125
  /**
99
126
  * Abstract method for message processing logic.
100
127
  */
@@ -20,7 +20,7 @@ export abstract class Msg91BaseSMSService implements ISMS {
20
20
  throw new Error(`Msg91 does not support sending plain text messages, you need to register a template and use the templateId to send the SMS.`);
21
21
  }
22
22
 
23
- async sendSMSUsingTemplate(to: string, templateName: string, templateParams: any, shouldQueueSms = false): Promise<void> {
23
+ async sendSMSUsingTemplate(to: string, templateName: string, templateParams: any, shouldQueueSms = false): Promise<any> {
24
24
  // Load template and evaluate it.
25
25
  const emailTemplate = await this.smsTemplateService.findOneByName(templateName);
26
26
  if (!emailTemplate) {
@@ -76,5 +76,5 @@ export abstract class Msg91BaseSMSService implements ISMS {
76
76
  this.logger.debug(`Queueing SMS to ${to} with message ${JSON.stringify(message)}`);
77
77
  }
78
78
 
79
- abstract sendSMSSynchronously(message: QueueMessage<any>): Promise<void>
79
+ abstract sendSMSSynchronously(message: QueueMessage<any>): Promise<any>
80
80
  }
@@ -28,7 +28,7 @@ export class Msg91OTPService extends Msg91BaseSMSService implements ISMS {
28
28
  super(commonConfiguration, 'OTPQueuePublisher', publisherFactory, smsTemplateService);
29
29
  }
30
30
 
31
- async sendSMSSynchronously(message: QueueMessage<any>): Promise<void> {
31
+ async sendSMSSynchronously(message: QueueMessage<any>): Promise<any> {
32
32
  const { to, templateId, otp } = message.payload;
33
33
  const params = { otp, template_id: templateId, mobile: to, authkey: this.commonConfiguration.msg91Sms.apiKey }
34
34
  const otpUrl = `${this.commonConfiguration.msg91Sms.url}/otp?${this.paramsToQueryString(params)}`;
@@ -21,7 +21,7 @@ export class Msg91SMSService extends Msg91BaseSMSService implements ISMS {
21
21
  super(commonConfiguration, 'SmsQueuePublisher', publisherFactory, smsTemplateService)
22
22
  }
23
23
 
24
- async sendSMSSynchronously(message: QueueMessage<any>): Promise<void> {
24
+ async sendSMSSynchronously(message: QueueMessage<any>): Promise<any> {
25
25
  const { to, templateId, ...templateParams } = message.payload;
26
26
  const body = { template_id: templateId, short_url: "0", recipients: [{ mobiles: to, ...templateParams }] };
27
27
  const headers = { "authkey": this.commonConfiguration.msg91Sms.apiKey };