@solidxai/core 0.1.1 → 0.1.2

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": "@solidxai/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "This module is a NestJS module containing all the required core providers required by a Solid application",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -73,7 +73,7 @@ export class SchematicService {
73
73
  })
74
74
  .map((fieldName) => {
75
75
  // Using argument array eliminates the need for shell-specific quoting
76
- return `--fieldNamesForRemoval=${fieldName}`;
76
+ return `--field-names-for-removal=${fieldName}`;
77
77
  });
78
78
  }
79
79
 
@@ -654,7 +654,8 @@ export class ModuleMetadataSeederService {
654
654
  action,
655
655
  module,
656
656
  parentMenuItem,
657
- sequenceNumber: m.sequenceNumber
657
+ sequenceNumber: m.sequenceNumber,
658
+ iconName: m.iconName,
658
659
  };
659
660
 
660
661
  // If existing, set its id so save() will perform an update, otherwise insert
@@ -5899,7 +5899,8 @@
5899
5899
  "sequenceNumber": 1,
5900
5900
  "actionUserKey": "appBuilder-root",
5901
5901
  "moduleUserKey": "solid-core",
5902
- "parentMenuItemUserKey": ""
5902
+ "parentMenuItemUserKey": "",
5903
+ "iconName" : "app_registration"
5903
5904
  },
5904
5905
  {
5905
5906
  "displayName": "Module",
@@ -5931,7 +5932,8 @@
5931
5932
  "sequenceNumber": 2,
5932
5933
  "actionUserKey": "layoutBuilder-root",
5933
5934
  "moduleUserKey": "solid-core",
5934
- "parentMenuItemUserKey": ""
5935
+ "parentMenuItemUserKey": "",
5936
+ "iconName": "space_dashboard"
5935
5937
  },
5936
5938
  {
5937
5939
  "displayName": "Menu Item",
@@ -5971,7 +5973,8 @@
5971
5973
  "sequenceNumber": 3,
5972
5974
  "actionUserKey": "media-root",
5973
5975
  "moduleUserKey": "solid-core",
5974
- "parentMenuItemUserKey": ""
5976
+ "parentMenuItemUserKey": "",
5977
+ "iconName":"perm_media"
5975
5978
  },
5976
5979
  {
5977
5980
  "displayName": "Media",
@@ -5995,7 +5998,8 @@
5995
5998
  "sequenceNumber": 4,
5996
5999
  "actionUserKey": "iam-root",
5997
6000
  "moduleUserKey": "solid-core",
5998
- "parentMenuItemUserKey": ""
6001
+ "parentMenuItemUserKey": "",
6002
+ "iconName": "person_shield"
5999
6003
  },
6000
6004
  {
6001
6005
  "displayName": "User",
@@ -6043,7 +6047,8 @@
6043
6047
  "sequenceNumber": 5,
6044
6048
  "actionUserKey": "queues-root",
6045
6049
  "moduleUserKey": "solid-core",
6046
- "parentMenuItemUserKey": ""
6050
+ "parentMenuItemUserKey": "",
6051
+ "iconName":"low_priority"
6047
6052
  },
6048
6053
  {
6049
6054
  "displayName": "Messages",
@@ -6067,7 +6072,8 @@
6067
6072
  "sequenceNumber": 6,
6068
6073
  "actionUserKey": "notification-root",
6069
6074
  "moduleUserKey": "solid-core",
6070
- "parentMenuItemUserKey": ""
6075
+ "parentMenuItemUserKey": "",
6076
+ "iconName":"notification_settings"
6071
6077
  },
6072
6078
  {
6073
6079
  "displayName": "Email",
@@ -6091,7 +6097,8 @@
6091
6097
  "sequenceNumber": 7,
6092
6098
  "actionUserKey": "other-root",
6093
6099
  "moduleUserKey": "solid-core",
6094
- "parentMenuItemUserKey": ""
6100
+ "parentMenuItemUserKey": "",
6101
+ "iconName":"other_admission"
6095
6102
  },
6096
6103
  {
6097
6104
  "displayName": "List of Values",
@@ -6171,7 +6178,8 @@
6171
6178
  "sequenceNumber": 8,
6172
6179
  "actionUserKey": "dasbhoard-root",
6173
6180
  "moduleUserKey": "solid-core",
6174
- "parentMenuItemUserKey": ""
6181
+ "parentMenuItemUserKey": "",
6182
+ "iconName" : "dashboard_customize"
6175
6183
  },
6176
6184
  {
6177
6185
  "displayName": "Dashboard",
@@ -6195,7 +6203,8 @@
6195
6203
  "sequenceNumber": 9,
6196
6204
  "actionUserKey": "settings-root",
6197
6205
  "moduleUserKey": "solid-core",
6198
- "parentMenuItemUserKey": ""
6206
+ "parentMenuItemUserKey": "",
6207
+ "iconName":"settings"
6199
6208
  },
6200
6209
  {
6201
6210
  "displayName": "App Settings",
@@ -11146,6 +11155,22 @@
11146
11155
  "sortable": true,
11147
11156
  "filterable": true
11148
11157
  }
11158
+ },
11159
+ {
11160
+ "type": "field",
11161
+ "attrs": {
11162
+ "name": "userAgent",
11163
+ "sortable": true,
11164
+ "filterable": true
11165
+ }
11166
+ },
11167
+ {
11168
+ "type": "field",
11169
+ "attrs": {
11170
+ "name": "createdAt",
11171
+ "sortable": true,
11172
+ "filterable": true
11173
+ }
11149
11174
  }
11150
11175
  ]
11151
11176
  }
@@ -876,7 +876,8 @@ export class ModelMetadataService {
876
876
  sequenceNumber: 1,
877
877
  actionUserKey: actionName,
878
878
  moduleUserKey: `${model.module.name}`,
879
- parentMenuItemUserKey: ""
879
+ parentMenuItemUserKey: "",
880
+ iconName : ""
880
881
  };
881
882
 
882
883
  const modelListview = {
@@ -181,7 +181,8 @@ export class ModuleMetadataService {
181
181
  sequenceNumber: 1,
182
182
  actionUserKey: `${module?.name}-home-action`,
183
183
  moduleUserKey: module?.name,
184
- parentMenuItemUserKey: ""
184
+ parentMenuItemUserKey: "",
185
+ iconName : "home"
185
186
  }
186
187
  ],
187
188
  views: [],
@@ -10,11 +10,14 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
10
10
  private readonly logger = new Logger(RabbitMqSubscriber.name);
11
11
  private readonly url: string;
12
12
  private readonly serviceRole: string;
13
-
14
- constructor(
15
- protected readonly mqMessageService: MqMessageService,
16
- protected readonly mqMessageQueueService: MqMessageQueueService,
17
- ) {
13
+ private connection: amqp.Connection | null = null;
14
+ private channel: amqp.Channel | null = null;
15
+ private consumerTag: string | null = null;
16
+ private reconnectPromise: Promise<void> | null = null;
17
+ private reconnectAttempt = 0;
18
+ private stopping = false;
19
+
20
+ constructor(protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService) {
18
21
  this.url = process.env.QUEUES_RABBIT_MQ_URL;
19
22
  this.serviceRole = process.env.QUEUES_SERVICE_ROLE;
20
23
  if (!this.url) {
@@ -46,6 +49,7 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
46
49
  username: url.username,
47
50
  password: decodeURIComponent(url.password),
48
51
  frameMax: 131072,
52
+ heartbeat: 30,
49
53
  });
50
54
 
51
55
  return connection
@@ -76,86 +80,235 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
76
80
  }
77
81
  }
78
82
 
79
- // this.logger.debug(`RabbitMqSubscriber instance created with options: ${JSON.stringify(this.options())} and url: ${this.url}`);
80
- // const connection = await amqp.connect(this.url);
81
-
82
- let connection;
83
83
  try {
84
- connection = await this.establishConnection();
85
- // this.logger.debug(`RabbitMqSubscriber connection established: ${JSON.stringify(this.options())} and url: ${this.url}`);
84
+ await this.connectAndConsume(queueName);
85
+ } catch (err) {
86
+ this.logger.error(`Failed to connect to RabbitMQ for queue ${queueName}: ${(err as Error).message}`, (err as Error).stack);
87
+ this.triggerReconnect(queueName, 'initial connection failure');
86
88
  }
87
- catch (err) {
88
- this.logger.error(`Failed to connect to RabbitMQ: ${(err as Error).message}`, (err as Error).stack);
89
- throw err;
89
+
90
+ this.logger.log(`RabbitMqSubscriber ready to consume messages: ${JSON.stringify(this.options())} and url: ${this.url}`);
91
+ }
92
+ }
93
+
94
+ private async connectAndConsume(queueName: string): Promise<void> {
95
+ await this.cleanup();
96
+
97
+ let connection: amqp.Connection;
98
+ try {
99
+ connection = await this.establishConnection();
100
+ } catch (err) {
101
+ this.logger.error(`Failed to connect to RabbitMQ for queue ${queueName}: ${(err as Error).message}`, (err as Error).stack);
102
+ throw err;
103
+ }
104
+
105
+ this.connection = connection;
106
+
107
+ connection.on('error', (err) => {
108
+ if (connection !== this.connection) return;
109
+ this.logger.error(`RabbitMqSubscriber connection error for queue ${queueName}: ${(err as Error).message}`);
110
+ });
111
+
112
+ connection.on('close', () => {
113
+ if (connection !== this.connection) return;
114
+ this.logger.warn(`RabbitMqSubscriber connection closed for queue ${queueName}`);
115
+ this.triggerReconnect(queueName, 'connection closed');
116
+ });
117
+
118
+ const channel = await connection.createChannel();
119
+ this.channel = channel;
120
+
121
+ channel.on('error', (err) => {
122
+ if (channel !== this.channel) return;
123
+ this.logger.error(`RabbitMqSubscriber channel error for queue ${queueName}: ${(err as Error).message}`);
124
+ });
125
+
126
+ channel.on('close', () => {
127
+ if (channel !== this.channel) return;
128
+ this.logger.warn(`RabbitMqSubscriber channel closed for queue ${queueName}`);
129
+ this.triggerReconnect(queueName, 'channel closed');
130
+ });
131
+
132
+ // Process one message at a time per consumer to avoid parallel work on the same subscriber instance.
133
+ await channel.prefetch(1);
134
+
135
+ // Use a direct exchange with a stable routing key so retry DLX can route back to the main queue.
136
+ const exchangeName = `${queueName}.exchange`;
137
+ const routingKey = `${queueName}.routing-key`;
138
+ const retryQueue = `${queueName}.retry`;
139
+ const failedQueue = `${queueName}.failed`;
140
+
141
+ await channel.assertExchange(exchangeName, 'direct', {});
142
+ await channel.assertQueue(queueName, {});
143
+ await channel.bindQueue(queueName, exchangeName, routingKey);
144
+
145
+ // Retry queue uses DLX to route expired messages back to the main exchange/routing key.
146
+ await channel.assertQueue(retryQueue, {
147
+ arguments: {
148
+ 'x-dead-letter-exchange': exchangeName,
149
+ 'x-dead-letter-routing-key': routingKey,
90
150
  }
151
+ });
91
152
 
92
- const channel = await connection.createChannel();
93
- // this.logger.debug(`RabbitMqSubscriber channel created: ${JSON.stringify(this.options())} and url: ${url}`);
153
+ await channel.assertQueue(failedQueue, {});
94
154
 
95
- const exchangeName = `${queueName}.exchange`;
96
- const routingKey = `${queueName}.routing-key`;
155
+ const consumeResult = await channel.consume(
156
+ queueName,
157
+ async (rawMessage) => {
158
+ if (!rawMessage) {
159
+ return;
160
+ }
97
161
 
98
- await channel.assertExchange(exchangeName, 'direct', {});
99
- // this.logger.debug(`RabbitMqSubscriber channel asserted: ${JSON.stringify(this.options())} and url: ${url}`);
162
+ const messageContentString = rawMessage.content.toString();
163
+ let message: QueueMessage<T> = null;
100
164
 
101
- const queue = await channel.assertQueue(queueName, {});
102
- // this.logger.debug(`RabbitMqSubscriber queue asserted: ${JSON.stringify(this.options())} and url: ${url}`);
165
+ try {
166
+ message = JSON.parse(messageContentString) as QueueMessage<T>;
167
+ } catch (error) {
168
+ this.logger.error(`Invalid JSON message on queue ${queueName}: ${(error as Error).message}`);
169
+ await this.publishToFailedQueue(queueName, rawMessage.content, channel, error);
170
+ channel.ack(rawMessage);
171
+ return;
172
+ }
103
173
 
104
- await channel.bindQueue(queue.queue, exchangeName, routingKey);
105
- // this.logger.debug(`RabbitMqSubscriber queue bound: ${JSON.stringify(this.options())} and url: ${url}`);
174
+ if (!message.retryCount) message.retryCount = 0;
175
+ if (!message.retryInterval) message.retryInterval = 1000;
176
+ if (!message.currentRetry) message.currentRetry = 0;
106
177
 
107
- // Consume messages from the queue
108
- channel.consume(
109
- queue.queue,
110
- async (rawMessage) => {
111
- if (rawMessage) {
112
- const messageContentString = rawMessage.content.toString();
113
- // this.logger.debug(`RabbitMqSubscriber Received raw message: ${messageContentString}`);
178
+ try {
179
+ await this.processMessage(message, rawMessage, channel);
180
+ } catch (error) {
181
+ await this.handleProcessingError(message, rawMessage, channel, error, queueName);
182
+ }
183
+ },
184
+ // Explicit ack enables reliable processing and retry routing.
185
+ { noAck: false },
186
+ );
114
187
 
115
- let message: QueueMessage<T> = null;
188
+ this.consumerTag = consumeResult.consumerTag;
189
+ }
116
190
 
117
- try {
118
- message = JSON.parse(messageContentString) as QueueMessage<T>;
191
+ // Retry flow: update DB -> increment retry -> send to retry queue with per-message expiration -> ack original.
192
+ private async handleProcessingError(message: QueueMessage<T>, rawMessage: amqp.ConsumeMessage, channel: amqp.Channel, error: any, queueName: string): Promise<void> {
193
+ const errorMessage = (error as Error)?.message || String(error);
194
+ this.logger.error(`Error processing message on queue ${queueName}: ${errorMessage}`);
119
195
 
120
- // this is the first time we are receiving the message so we set the currentRetry to 0
121
- if (!message.retryCount) message.retryCount = 0;
122
- if (!message.retryInterval) message.retryInterval = 1000;
123
- if (!message.currentRetry) message.currentRetry = 0;
196
+ if (message.currentRetry < message.retryCount) {
197
+ await this.updateStatusInDatabase('retrying', message);
124
198
 
125
- await this.processMessage(message, rawMessage, channel);
126
- }
127
- catch (error) {
128
- this.logger.error(`Error processing message: ${error.message}`);
199
+ message.currentRetry++;
200
+ const retryQueue = `${queueName}.retry`;
201
+ const payload = Buffer.from(JSON.stringify(message));
129
202
 
130
- // if an error occurs then if retryCount is set we start retrying.
131
- if (message) {
132
- if (message.currentRetry < message.retryCount) {
133
- await this.updateStatusInDatabase('retrying', message);
203
+ // Per-message expiration keeps the message in the retry queue until TTL, then DLX routes it back.
204
+ channel.sendToQueue(retryQueue, payload, {
205
+ expiration: String(message.retryInterval || 1000),
206
+ headers: {
207
+ 'x-error': errorMessage,
208
+ }
209
+ });
134
210
 
135
- message.currentRetry++;
136
- this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms`);
137
- setTimeout(() => {
138
- this.retryMessage(message, rawMessage, channel);
139
- }, message.retryInterval);
140
- } else {
141
- await this.updateStatusInDatabase('failed', message, error.message, '');
211
+ channel.ack(rawMessage);
212
+ this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms on queue ${queueName}`);
213
+ return;
214
+ }
142
215
 
143
- this.logger.error(`Message failed after ${message.retryCount} attempts: ${error.message}`);
144
- channel.ack(rawMessage); // Discard the message after max retries
145
- }
146
- }
216
+ await this.updateStatusInDatabase('failed', message, errorMessage, '');
217
+ channel.ack(rawMessage);
218
+ await this.publishToFailedQueue(queueName, Buffer.from(JSON.stringify(message)), channel, error);
219
+ this.logger.error(`Message failed after ${message.retryCount} attempts on queue ${queueName}: ${errorMessage}`);
220
+ }
147
221
 
148
- }
149
- }
150
- },
151
- // { noAck: true },
152
- {},
153
- );
222
+ private async publishToFailedQueue(queueName: string, payload: Buffer | string, channel: amqp.Channel, error?: any): Promise<void> {
223
+ const failedQueue = `${queueName}.failed`;
224
+ const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
225
+ const errorMessage = (error as Error)?.message || String(error || '');
154
226
 
155
- this.logger.log(`RabbitMqSubscriber ready to consume messages: ${JSON.stringify(this.options())} and url: ${this.url}`);
227
+ try {
228
+ channel.sendToQueue(failedQueue, body, errorMessage ? {
229
+ headers: { 'x-error': errorMessage }
230
+ } : undefined);
231
+ } catch (err) {
232
+ this.logger.error(`Failed to publish to failed queue ${failedQueue}: ${(err as Error).message}`);
233
+ }
234
+ }
235
+
236
+ private triggerReconnect(queueName: string, reason: string) {
237
+ if (this.stopping) return;
238
+ if (this.reconnectPromise) return;
239
+
240
+ this.reconnectPromise = this.reconnectLoop(queueName, reason)
241
+ .finally(() => {
242
+ this.reconnectPromise = null;
243
+ });
244
+ }
245
+
246
+ // Reconnect with backoff to avoid hammering the broker during outages.
247
+ private async reconnectLoop(queueName: string, reason: string): Promise<void> {
248
+ this.logger.warn(`RabbitMqSubscriber reconnecting for queue ${queueName}: ${reason}`);
249
+
250
+ while (!this.stopping) {
251
+ try {
252
+ await this.connectAndConsume(queueName);
253
+ this.reconnectAttempt = 0;
254
+ this.logger.log(`RabbitMqSubscriber reconnected for queue ${queueName}`);
255
+ return;
256
+ } catch (err) {
257
+ this.reconnectAttempt += 1;
258
+ const delay = this.backoff();
259
+ this.logger.warn(`RabbitMqSubscriber reconnect failed for queue ${queueName}; retrying in ${delay}ms`);
260
+ await this.sleep(delay);
261
+ }
262
+ }
263
+ }
264
+
265
+ private async cleanup(): Promise<void> {
266
+ const channel = this.channel;
267
+ const connection = this.connection;
268
+ const consumerTag = this.consumerTag;
269
+
270
+ this.channel = null;
271
+ this.connection = null;
272
+ this.consumerTag = null;
273
+
274
+ if (channel) {
275
+ try {
276
+ if (consumerTag) {
277
+ await channel.cancel(consumerTag);
278
+ }
279
+ } catch (_) {
280
+ // ignore
281
+ }
282
+
283
+ try {
284
+ await channel.close();
285
+ } catch (_) {
286
+ // ignore
287
+ }
288
+ }
289
+
290
+ if (connection) {
291
+ try {
292
+ await connection.close();
293
+ } catch (_) {
294
+ // ignore
295
+ }
156
296
  }
157
297
  }
158
298
 
299
+ private sleep(ms: number): Promise<void> {
300
+ return new Promise(resolve => setTimeout(resolve, ms));
301
+ }
302
+
303
+ // Exponential backoff with jitter, capped to 30s.
304
+ private backoff(): number {
305
+ const baseMs = 1000;
306
+ const maxMs = 30_000;
307
+ const exp = Math.min(maxMs, baseMs * Math.pow(2, this.reconnectAttempt));
308
+ const jitter = Math.floor(Math.random() * (exp * 0.2));
309
+ return Math.min(maxMs, exp + jitter);
310
+ }
311
+
159
312
  /**
160
313
  * Abstract method for message processing logic.
161
314
  */
@@ -168,43 +321,14 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
168
321
  // Ack the message.
169
322
  channel.ack(rawMessage);
170
323
 
171
- // TODO: Update the database to indicate that the task is finished.
324
+ // Persist success output and timing.
172
325
  await this.updateStatusInDatabase('succeeded', message, '', result ? JSON.stringify(result, null, 2) : '');
173
326
 
174
327
  }
175
328
 
176
- /**
177
- * Retry the message by invoking the processing logic again.
178
- */
179
- private async retryMessage(message: QueueMessage<T>, rawMessage, channel) {
180
- try {
181
- await this.processMessage(message, rawMessage, channel);
182
- } catch (error) {
183
- if (message.currentRetry < message.retryCount) {
184
- await this.updateStatusInDatabase('retrying', message);
185
-
186
- message.currentRetry++;
187
- this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms: ${error.message}`);
188
- setTimeout(() => {
189
- this.retryMessage(message, rawMessage, channel);
190
- }, message.retryInterval);
191
- } else {
192
-
193
- this.logger.error(`Message failed after ${message.retryCount} attempts: ${error.message}`);
194
-
195
- // Discard the message after max retries
196
- channel.ack(rawMessage);
197
-
198
- // TODO: Store the error in the database and update the status accordingly.
199
- await this.updateStatusInDatabase('failed', message, error.message, '');
200
-
201
- }
202
- }
203
- }
204
-
205
329
  private async updateStatusInDatabase(stage: string, message: QueueMessage<T>, error: string = '', result: string = '') {
206
330
 
207
- // TODO: make an entry in the relevant database table, generate a unique id earlier.
331
+ // Update the existing message record by messageId; creation happens upstream.
208
332
  try {
209
333
  // 1. resolve the queue first
210
334
  const mqMessage = await this.mqMessageService.repo.findOne({