@wabot-dev/framework 0.2.6 → 0.4.0-beta.1

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 (36) hide show
  1. package/README.md +9 -12
  2. package/dist/src/addon/async/pg/PgJobRepository.js +50 -2
  3. package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +5 -0
  4. package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +7 -2
  5. package/dist/src/addon/chat-bot/openia/OpenaiChatAdapter.js +15 -3
  6. package/dist/src/addon/chat-bot/pg/PgChatMemory.js +5 -0
  7. package/dist/src/addon/chat-bot/pg/PgChatRepository.js +5 -0
  8. package/dist/src/addon/chat-bot/wabot/WabotChatAdapter.js +1 -1
  9. package/dist/src/addon/chat-controller/whatsapp/PgWhatsAppRepository.js +5 -0
  10. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppReceiverByWabotProxy.js +1 -0
  11. package/dist/src/core/lock/Locker.js +7 -0
  12. package/dist/src/feature/async/@command.js +3 -3
  13. package/dist/src/feature/async/@commandHandler.js +3 -3
  14. package/dist/src/feature/async/Async.js +21 -14
  15. package/dist/src/feature/async/{CommandMetadataStore.js → AsyncMetadataStore.js} +27 -5
  16. package/dist/src/feature/async/Job.js +92 -2
  17. package/dist/src/feature/async/JobExecutor.js +62 -0
  18. package/dist/src/feature/async/JobRepository.js +9 -0
  19. package/dist/src/feature/async/JobRunner.js +17 -6
  20. package/dist/src/feature/async/JobScheduler.js +80 -0
  21. package/dist/src/feature/async/JobWatchdog.js +74 -0
  22. package/dist/src/feature/async/runCommandHandlers.js +19 -25
  23. package/dist/src/feature/chat-bot/Chat.js +3 -0
  24. package/dist/src/feature/chat-controller/runChatControllers.js +1 -1
  25. package/dist/src/feature/pg/PgCrudRepository.js +7 -1
  26. package/dist/src/feature/pg/PgLock.js +20 -0
  27. package/dist/src/feature/pg/PgLockKey.js +68 -0
  28. package/dist/src/feature/pg/PgRepositoryBase.js +40 -27
  29. package/dist/src/feature/pg/pgStorage.js +16 -0
  30. package/dist/src/feature/pg/withPgClient.js +45 -0
  31. package/dist/src/feature/pg/withPgTransaction.js +45 -0
  32. package/dist/src/index.d.ts +252 -96
  33. package/dist/src/index.js +8 -4
  34. package/package.json +8 -6
  35. package/dist/src/feature/async/Command.js +0 -9
  36. package/dist/src/feature/async/JobsEventsHub.js +0 -36
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  **Un framework moderno y flexible para crear bots con TypeScript e Inteligencia Artificial**
11
11
 
12
- [Documentación](https://docs.wabot.dev) • [Inicio Rápido](https://docs.wabot.dev/guides/start-new-project/) • [Repositorio](https://github.com/wabot-dev/wabot-ts)
12
+ [Documentación](https://docs.wabot.dev) • [Inicio Rápido](https://docs.wabot.dev/guides/start-new-project/)
13
13
 
14
14
  </div>
15
15
 
@@ -53,14 +53,13 @@ Wabot se integra nativamente con las principales plataformas de mensajería:
53
53
 
54
54
  ## 🧠 Proveedores de IA
55
55
 
56
- Conecta tu bot con los principales modelos de lenguaje del mercado:
57
-
58
- | Proveedor | Modelos |
59
- |-----------|---------|
60
- | 🟢 **OpenAI** | GPT-4, GPT-3.5, y más |
61
- | 🔵 **Google** | Gemini Pro, Gemini Ultra |
62
- | 🟣 **Anthropic** | Claude 3 y más |
56
+ Potencia tu bot con los principales proveedores de inteligencia artificial:
63
57
 
58
+ | Proveedor | Soporte en Wabot |
59
+ |-----------|------------------|
60
+ | 🟢 **OpenAI** | ✅ Integración completa |
61
+ | 🔵 **Google** | ✅ Integración completa |
62
+ | 🟣 **Anthropic** | ✅ Integración completa |
64
63
  ---
65
64
 
66
65
  ## 📚 Documentación
@@ -84,7 +83,6 @@ Explora nuestra documentación completa para dominar Wabot:
84
83
 
85
84
  ¿Quieres contribuir al proyecto? ¡Serás bienvenido!
86
85
 
87
- - 📦 **Repositorio:** [github.com/wabot-dev/wabot-ts](https://github.com/wabot-dev/wabot-ts)
88
86
  - 🐛 **Reportar bugs:** [Issues](https://github.com/wabot-dev/wabot-ts/issues)
89
87
  - 💬 **Discusiones:** [Discussions](https://github.com/wabot-dev/wabot-ts/discussions)
90
88
 
@@ -94,9 +92,8 @@ Explora nuestra documentación completa para dominar Wabot:
94
92
 
95
93
  ¿Necesitas ayuda? Estamos aquí para ti:
96
94
 
97
- - 📧 **Email:** [soporte@wabot.dev](mailto:soporte@wabot.dev)
95
+ - 📧 **Email:** [contact@wabot.dev](mailto:contact@wabot.dev)
98
96
  - 🐛 **Issues:** [Reportar un problema](https://github.com/wabot-dev/wabot-ts/issues)
99
- - 📖 **Documentación:** [docs.wabot.dev](https://docs.wabot.dev)
100
97
 
101
98
  ---
102
99
 
@@ -110,6 +107,6 @@ Este proyecto está licenciado bajo [Licencia MIT](https://github.com/wabot-dev/
110
107
 
111
108
  **Hecho con ❤️ por el equipo de Wabot**
112
109
 
113
- [Sitio Web](https://wabot.dev) • [Documentación](https://docs.wabot.dev) • [GitHub](https://github.com/wabot-dev/wabot-ts) • [npm](https://www.npmjs.com/package/@wabot-dev/framework)
110
+ [Sitio Web](https://wabot.dev) • [Documentación](https://docs.wabot.dev) • [npm](https://www.npmjs.com/package/@wabot-dev/framework)
114
111
 
115
112
  </div>
@@ -2,12 +2,18 @@ import { __decorate, __metadata } from 'tslib';
2
2
  import { Pool } from 'pg';
3
3
  import { singleton } from '../../../core/injection/index.js';
4
4
  import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
5
- import '../../../feature/async/CommandMetadataStore.js';
5
+ import '../../../feature/pg/PgLock.js';
6
+ import 'debug';
7
+ import 'node:crypto';
8
+ import { withPgClient } from '../../../feature/pg/withPgClient.js';
9
+ import '../../../feature/pg/pgStorage.js';
10
+ import '../../../feature/async/AsyncMetadataStore.js';
6
11
  import '../../../feature/async/Async.js';
7
12
  import { Job } from '../../../feature/async/Job.js';
8
13
  import '../../../feature/async/JobRepository.js';
9
14
  import '../../../feature/async/JobRunner.js';
10
- import '../../../feature/async/JobsEventsHub.js';
15
+ import '../../../feature/async/JobScheduler.js';
16
+ import '../../../feature/async/JobWatchdog.js';
11
17
 
12
18
  let PgJobRepository = class PgJobRepository extends PgCrudRepository {
13
19
  constructor(pool) {
@@ -17,6 +23,48 @@ let PgJobRepository = class PgJobRepository extends PgCrudRepository {
17
23
  constructor: Job,
18
24
  });
19
25
  }
26
+ async findPendingForRunFrom(date, limit) {
27
+ const sql = `
28
+ SELECT ${this.columns}
29
+ FROM ${this.table}
30
+ WHERE data ? 'scheduledAt'
31
+ AND (data->>'scheduledAt')::bigint <= $1
32
+ AND data->>'startedAt' IS NULL
33
+ AND data->>'successAt' IS NULL
34
+ AND data->>'failedAt' IS NULL
35
+ ORDER BY (data->>'scheduledAt')::bigint ASC
36
+ LIMIT $2
37
+ `;
38
+ const items = await this.query(sql, [date.getTime(), limit]);
39
+ return items;
40
+ }
41
+ async findRunningJobs() {
42
+ const sql = `
43
+ SELECT ${this.columns}
44
+ FROM ${this.table}
45
+ WHERE data ? 'startedAt'
46
+ AND data->>'startedAt' IS NOT NULL
47
+ AND data->>'successAt' IS NULL
48
+ AND data->>'failedAt' IS NULL
49
+ `;
50
+ const items = await this.query(sql, []);
51
+ return items;
52
+ }
53
+ async countRunningByCommand(commandName) {
54
+ const sql = `
55
+ SELECT COUNT(*)::int AS count
56
+ FROM ${this.table}
57
+ WHERE data ? 'startedAt'
58
+ AND data->>'startedAt' IS NOT NULL
59
+ AND data->>'successAt' IS NULL
60
+ AND data->>'failedAt' IS NULL
61
+ AND data->>'commandName' = $1
62
+ `;
63
+ return withPgClient(this.pool, async (client) => {
64
+ const result = await client.query(sql, [commandName]);
65
+ return result.rows[0]?.count ?? 0;
66
+ });
67
+ }
20
68
  };
21
69
  PgJobRepository = __decorate([
22
70
  singleton(),
@@ -1,5 +1,10 @@
1
1
  import { __decorate, __metadata } from 'tslib';
2
2
  import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
3
+ import '../../../feature/pg/PgLock.js';
4
+ import 'debug';
5
+ import 'node:crypto';
6
+ import '../../../feature/pg/withPgClient.js';
7
+ import '../../../feature/pg/pgStorage.js';
3
8
  import { Pool } from 'pg';
4
9
  import { ApiKey } from './ApiKey.js';
5
10
  import { CustomError } from '../../../core/error/CustomError.js';
@@ -1,9 +1,14 @@
1
1
  import { __decorate, __metadata } from 'tslib';
2
2
  import { singleton } from '../../../core/injection/index.js';
3
- import { Pool } from 'pg';
3
+ import { CustomError } from '../../../core/error/CustomError.js';
4
4
  import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
5
+ import '../../../feature/pg/PgLock.js';
6
+ import 'debug';
7
+ import 'node:crypto';
8
+ import '../../../feature/pg/withPgClient.js';
9
+ import '../../../feature/pg/pgStorage.js';
10
+ import { Pool } from 'pg';
5
11
  import { JwtRefreshToken } from './JwtRefreshToken.js';
6
- import { CustomError } from '../../../core/error/CustomError.js';
7
12
 
8
13
  let PgJwtRefreshTokenRepository = class PgJwtRefreshTokenRepository extends PgCrudRepository {
9
14
  constructor(pool) {
@@ -36,10 +36,22 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
36
36
  return openIaInput;
37
37
  }
38
38
  mapConectionMessage(item) {
39
- if (!item.text) {
40
- throw new Error('System message content is empty');
39
+ const content = [];
40
+ if (item.text)
41
+ content.push({ type: 'input_text', text: item.text });
42
+ if (item.images) {
43
+ for (const image of item.images) {
44
+ content.push({
45
+ type: 'input_image',
46
+ image_url: image.publicUrl ?? image.base64Url,
47
+ detail: 'auto',
48
+ });
49
+ }
50
+ }
51
+ if (content.length === 0) {
52
+ throw new Error('message content is empty');
41
53
  }
42
- return { role: 'user', content: item.text };
54
+ return { role: 'user', content };
43
55
  }
44
56
  mapBotMessage(item) {
45
57
  if (!item.text) {
@@ -1,4 +1,9 @@
1
1
  import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
2
+ import '../../../feature/pg/PgLock.js';
3
+ import 'debug';
4
+ import 'node:crypto';
5
+ import '../../../feature/pg/withPgClient.js';
6
+ import '../../../feature/pg/pgStorage.js';
2
7
  import '../../../feature/chat-bot/ChatBot.js';
3
8
  import { ChatItem } from '../../../feature/chat-bot/ChatItem.js';
4
9
  import '../../../core/injection/index.js';
@@ -3,6 +3,11 @@ import { Pool } from 'pg';
3
3
  import { PgChatMemory } from './PgChatMemory.js';
4
4
  import { singleton } from '../../../core/injection/index.js';
5
5
  import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
6
+ import '../../../feature/pg/PgLock.js';
7
+ import 'debug';
8
+ import 'node:crypto';
9
+ import '../../../feature/pg/withPgClient.js';
10
+ import '../../../feature/pg/pgStorage.js';
6
11
  import { Chat } from '../../../feature/chat-bot/Chat.js';
7
12
  import '../../../feature/chat-bot/ChatBot.js';
8
13
  import 'uuid';
@@ -16,7 +16,7 @@ let WabotChatAdapter = class WabotChatAdapter {
16
16
  }
17
17
  }
18
18
  async nextItems(req) {
19
- const response = await fetch(this.baseUrl + '/chat-bot/next-item', {
19
+ const response = await fetch(this.baseUrl + '/chat-bot/next-items', {
20
20
  method: 'post',
21
21
  headers: {
22
22
  Authorization: `Api-Key ${this.apiKey}`,
@@ -3,6 +3,11 @@ import { Pool } from 'pg';
3
3
  import { WhatsApp } from './WhatsApp.js';
4
4
  import { singleton } from '../../../core/injection/index.js';
5
5
  import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
6
+ import '../../../feature/pg/PgLock.js';
7
+ import 'debug';
8
+ import 'node:crypto';
9
+ import '../../../feature/pg/withPgClient.js';
10
+ import '../../../feature/pg/pgStorage.js';
6
11
 
7
12
  let PgWhatsAppRepository = class PgWhatsAppRepository extends PgCrudRepository {
8
13
  constructor(pool) {
@@ -44,6 +44,7 @@ let WhatsAppReceiverByWabotProxy = class WhatsAppReceiverByWabotProxy extends Wh
44
44
  this.loger.trace(`success receive message from '${data.from}' to '${data.to}'`);
45
45
  request.listener({
46
46
  chatConnection: {
47
+ chatType: 'PRIVATE',
47
48
  channelName: 'WhatsAppChannel',
48
49
  id: data.from,
49
50
  },
@@ -0,0 +1,7 @@
1
+ class Locker {
2
+ withKey(key) {
3
+ throw new Error('Not implemented');
4
+ }
5
+ }
6
+
7
+ export { Locker };
@@ -1,10 +1,10 @@
1
- import { CommandMetadataStore } from './CommandMetadataStore.js';
1
+ import { AsyncMetadataStore } from './AsyncMetadataStore.js';
2
2
  import { container } from '../../core/injection/index.js';
3
3
 
4
4
  function command(config) {
5
5
  return function (target) {
6
- const handlerContainer = container.resolve(CommandMetadataStore);
7
- handlerContainer.registerCommand(target, config?.name ?? target.name);
6
+ const metadataStore = container.resolve(AsyncMetadataStore);
7
+ metadataStore.registerCommand(target, typeof config === 'string' ? config : config.name);
8
8
  };
9
9
  }
10
10
 
@@ -1,10 +1,10 @@
1
- import { CommandMetadataStore } from './CommandMetadataStore.js';
1
+ import { AsyncMetadataStore } from './AsyncMetadataStore.js';
2
2
  import { container, injectable } from '../../core/injection/index.js';
3
3
 
4
4
  function commandHandler(config) {
5
5
  return function (target) {
6
- const handlerContainer = container.resolve(CommandMetadataStore);
7
- handlerContainer.registerHandler(config.command, target);
6
+ const metadataStore = container.resolve(AsyncMetadataStore);
7
+ metadataStore.registerCommandHandler(typeof config === 'function' ? config : config.command, target);
8
8
  injectable()(target);
9
9
  };
10
10
  }
@@ -1,38 +1,45 @@
1
1
  import { __decorate, __metadata } from 'tslib';
2
2
  import { singleton } from '../../core/injection/index.js';
3
- import { CommandMetadataStore } from './CommandMetadataStore.js';
3
+ import { AsyncMetadataStore } from './AsyncMetadataStore.js';
4
4
  import { Job } from './Job.js';
5
5
  import { JobRepository } from './JobRepository.js';
6
- import { JobsEventsHub } from './JobsEventsHub.js';
6
+ import { JobScheduler } from './JobScheduler.js';
7
+ import '../../core/validation/metadata/ValidationMetadataStore.js';
8
+ import { validateAndTransform } from '../../core/validation/validateAndTransform.js';
7
9
 
8
10
  let Async = class Async {
9
11
  jobRepository;
10
- handlerContainer;
11
- jobsEventsHub;
12
- constructor(jobRepository, handlerContainer, jobsEventsHub) {
12
+ metadataStore;
13
+ jobScheduler;
14
+ constructor(jobRepository, metadataStore, jobScheduler) {
13
15
  this.jobRepository = jobRepository;
14
- this.handlerContainer = handlerContainer;
15
- this.jobsEventsHub = jobsEventsHub;
16
+ this.metadataStore = metadataStore;
17
+ this.jobScheduler = jobScheduler;
16
18
  }
17
- async run(command) {
18
- const commandName = this.handlerContainer.getCommandName(command.constructor);
19
+ async runCommand(ctor, data) {
20
+ const commandName = this.metadataStore.getCommandName(ctor);
19
21
  if (!commandName) {
20
- throw new Error(`${command.constructor.name} is not registered as command`);
22
+ throw new Error(`${ctor.name} is not registered as command`);
23
+ }
24
+ const { error, value: commandData } = validateAndTransform(data, ctor);
25
+ if (!commandData) {
26
+ throw new Error('Invalid command data');
21
27
  }
22
28
  const job = new Job({
23
29
  commandName,
24
- commandData: command['data'],
30
+ commandData,
31
+ scheduledAt: new Date().getTime(),
25
32
  });
26
33
  await this.jobRepository.create(job);
27
- this.jobsEventsHub.notifyJobCreated(job);
34
+ this.jobScheduler.tryExecuteNow(job);
28
35
  return job;
29
36
  }
30
37
  };
31
38
  Async = __decorate([
32
39
  singleton(),
33
40
  __metadata("design:paramtypes", [JobRepository,
34
- CommandMetadataStore,
35
- JobsEventsHub])
41
+ AsyncMetadataStore,
42
+ JobScheduler])
36
43
  ], Async);
37
44
 
38
45
  export { Async };
@@ -1,16 +1,32 @@
1
1
  import { __decorate } from 'tslib';
2
2
  import { singleton } from '../../core/injection/index.js';
3
3
 
4
- let CommandMetadataStore = class CommandMetadataStore {
4
+ let AsyncMetadataStore = class AsyncMetadataStore {
5
5
  handlersMap = new Map();
6
6
  handlersInverseMap = new Map();
7
7
  commandsMap = new Map();
8
8
  commandsInverseMap = new Map();
9
+ cronsMap = new Map();
10
+ registerCron(cron, config) {
11
+ let ctorCrons = this.cronsMap.get(cron);
12
+ if (!ctorCrons) {
13
+ this.cronsMap.set(cron, (ctorCrons = []));
14
+ }
15
+ ctorCrons.push({ handlerConstructor: cron, config });
16
+ this.handlersMap.set(config.commandName, cron);
17
+ }
18
+ requireCronMetadata(cron) {
19
+ const metadata = this.cronsMap.get(cron);
20
+ if (!metadata) {
21
+ throw new Error(`cron ${cron.name} is not registered`);
22
+ }
23
+ return metadata;
24
+ }
9
25
  registerCommand(command, commandName) {
10
26
  this.commandsMap.set(commandName, command);
11
27
  this.commandsInverseMap.set(command, commandName);
12
28
  }
13
- registerHandler(command, handlerConstructor) {
29
+ registerCommandHandler(command, handlerConstructor) {
14
30
  let commandName = this.commandsInverseMap.get(command);
15
31
  if (!commandName) {
16
32
  throw new Error(`Should use @command decorator on command class ${command.name}`);
@@ -24,6 +40,12 @@ let CommandMetadataStore = class CommandMetadataStore {
24
40
  getCommandNameForHandler(handlerConstructor) {
25
41
  return this.handlersInverseMap.get(handlerConstructor) ?? null;
26
42
  }
43
+ requireCommandNameForHandler(handlerConstructor) {
44
+ const commmandName = this.handlersInverseMap.get(handlerConstructor) ?? null;
45
+ if (!commmandName)
46
+ throw new Error(`Can't found a registered command for ${handlerConstructor.name}`);
47
+ return commmandName;
48
+ }
27
49
  getCommandName(command) {
28
50
  return this.commandsInverseMap.get(command) ?? null;
29
51
  }
@@ -31,8 +53,8 @@ let CommandMetadataStore = class CommandMetadataStore {
31
53
  return this.commandsMap.get(commandName) ?? null;
32
54
  }
33
55
  };
34
- CommandMetadataStore = __decorate([
56
+ AsyncMetadataStore = __decorate([
35
57
  singleton()
36
- ], CommandMetadataStore);
58
+ ], AsyncMetadataStore);
37
59
 
38
- export { CommandMetadataStore };
60
+ export { AsyncMetadataStore };
@@ -5,22 +5,112 @@ class Job extends Entity {
5
5
  get commandName() {
6
6
  return this.data.commandName;
7
7
  }
8
+ get commandData() {
9
+ return this.data.commandData;
10
+ }
11
+ get reintentsDelaysInSeconds() {
12
+ return this.data.reintentsDelaysInSeconds;
13
+ }
14
+ get aceptableRunningTimeSeconds() {
15
+ return this.data.aceptableRunningTimeSeconds;
16
+ }
17
+ get stuckRetryAttempts() {
18
+ return this.data.stuckRetryAttempts;
19
+ }
20
+ get runningSeconds() {
21
+ if (!this.isRunning())
22
+ return -1;
23
+ const now = new Date().getTime();
24
+ return (now - this.data.startedAt) / 1000;
25
+ }
26
+ get successAt() {
27
+ return this.data.successAt != null ? new Date(this.data.successAt) : null;
28
+ }
29
+ get failedAt() {
30
+ return this.data.failedAt != null ? new Date(this.data.failedAt) : null;
31
+ }
32
+ get scheduledAt() {
33
+ return this.data.scheduledAt != null ? new Date(this.data.scheduledAt) : null;
34
+ }
35
+ get intentNumber() {
36
+ return this.data.intentNumber ?? 0;
37
+ }
38
+ wasSuccess() {
39
+ return this.successAt != null;
40
+ }
41
+ wasFailed() {
42
+ return this.data.failedAt != null;
43
+ }
8
44
  hasFinished() {
9
- return this.data.successAt != null || this.data.failedAt != null;
45
+ return this.wasSuccess() || this.wasFailed();
46
+ }
47
+ isRunning() {
48
+ return (!this.hasFinished() &&
49
+ this.data.scheduledAt != null &&
50
+ this.data.startedAt != null &&
51
+ this.data.startedAt >= this.data.scheduledAt &&
52
+ this.data.startedAt <= new Date().getTime() &&
53
+ this.data.intentNumber != null);
54
+ }
55
+ isScheduleReady() {
56
+ return (!this.hasFinished() &&
57
+ !this.isRunning() &&
58
+ this.data.scheduledAt != null &&
59
+ this.data.scheduledAt <= new Date().getTime());
60
+ }
61
+ isStuck() {
62
+ return this.runningSeconds > (this.data.aceptableRunningTimeSeconds ?? 900);
10
63
  }
11
64
  setAsStarted() {
65
+ if (!this.isScheduleReady())
66
+ throw new Error(`job ${this.id} can't be started without ready schedule`);
12
67
  this.data.startedAt = new Date().getTime();
68
+ this.data.intentNumber = this.data.intentNumber == null ? 0 : this.data.intentNumber + 1;
13
69
  }
14
70
  setAsSuccess() {
71
+ if (this.hasFinished())
72
+ throw new Error(`job ${this.id} Can't be set as success because has be finished previously`);
73
+ if (!this.isRunning())
74
+ throw new Error(`job ${this.id} can't be set as success because is no running`);
15
75
  this.data.successAt = new Date().getTime();
16
76
  }
17
77
  setAsFailed(error) {
18
- this.data.failedAt = new Date().getTime();
78
+ if (this.hasFinished())
79
+ throw new Error(`job ${this.id} Can't be set as failed because has be finished previously`);
80
+ if (!this.isRunning())
81
+ throw new Error(`job ${this.id} can't be set as failed because is no running`);
82
+ const now = new Date().getTime();
19
83
  this.data.error = {
84
+ time: now,
20
85
  message: error.message,
21
86
  stack: error.stack,
22
87
  info: error instanceof CustomError ? error.info : undefined,
23
88
  };
89
+ if (this.data.intentNumber == null)
90
+ throw new Error('Invalid intent number');
91
+ const currentReintentDelay = (this.data.reintentsDelaysInSeconds ?? []).at(this.data.intentNumber);
92
+ if (currentReintentDelay == null) {
93
+ this.data.failedAt = now;
94
+ }
95
+ else {
96
+ this.data.scheduledAt = now + currentReintentDelay * 1000;
97
+ this.data.startedAt = undefined;
98
+ }
99
+ }
100
+ recover() {
101
+ if (!this.isStuck())
102
+ throw new Error(`job ${this.id} Can't be recovered because is not stuck`);
103
+ const now = Date.now();
104
+ this.data.intentNumber = (this.data.intentNumber ?? 0) + 1;
105
+ const configuredAttempts = this.data.stuckRetryAttempts ?? 2;
106
+ if (this.data.intentNumber <= configuredAttempts) {
107
+ this.data.scheduledAt = now;
108
+ this.data.startedAt = undefined;
109
+ }
110
+ else {
111
+ this.data.failedAt = now;
112
+ this.data.error = { time: now, message: 'Job stuck and exceeded maximum retries' };
113
+ }
24
114
  }
25
115
  }
26
116
 
@@ -0,0 +1,62 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import { singleton } from '../../core/injection/index.js';
3
+ import { JobRunner } from './JobRunner.js';
4
+ import { JobRepository } from './JobRepository.js';
5
+ import { Env } from '../../core/env/Env.js';
6
+ import { Logger } from '../../core/logger/Logger.js';
7
+ import { Locker } from '../../core/lock/Locker.js';
8
+
9
+ let JobExecutor = class JobExecutor {
10
+ locker;
11
+ runner;
12
+ repo;
13
+ env;
14
+ activeJobs = 0;
15
+ logger = new Logger('wabot:job-executor');
16
+ constructor(locker, runner, repo, env) {
17
+ this.locker = locker;
18
+ this.runner = runner;
19
+ this.repo = repo;
20
+ this.env = env;
21
+ }
22
+ remainingSlots() {
23
+ const max = this.env.requireNumber('WABOT_JOB_EXECUTOR_MAX_CONCURRENT_JOBS', { default: 5 });
24
+ return max - this.activeJobs;
25
+ }
26
+ async execute(job) {
27
+ if (!this.tryAcquire())
28
+ return;
29
+ try {
30
+ await this.locker.withKey(`wabot-job-${job.id}`).tryRun(async () => {
31
+ const fresh = await this.repo.findOrThrow(job.id);
32
+ if (!fresh.isScheduleReady())
33
+ return;
34
+ await this.runner.run(fresh);
35
+ });
36
+ }
37
+ catch (e) {
38
+ this.logger.error(e);
39
+ }
40
+ finally {
41
+ this.release();
42
+ }
43
+ }
44
+ tryAcquire() {
45
+ if (this.remainingSlots() <= 0)
46
+ return false;
47
+ this.activeJobs++;
48
+ return true;
49
+ }
50
+ release() {
51
+ this.activeJobs = Math.max(0, this.activeJobs - 1);
52
+ }
53
+ };
54
+ JobExecutor = __decorate([
55
+ singleton(),
56
+ __metadata("design:paramtypes", [Locker,
57
+ JobRunner,
58
+ JobRepository,
59
+ Env])
60
+ ], JobExecutor);
61
+
62
+ export { JobExecutor };
@@ -2,6 +2,12 @@ import { __decorate } from 'tslib';
2
2
  import { singleton } from '../../core/injection/index.js';
3
3
 
4
4
  let JobRepository = class JobRepository {
5
+ findRunningJobs() {
6
+ throw new Error('Method not implemented.');
7
+ }
8
+ findPendingForRunFrom(date, limit) {
9
+ throw new Error('Method not implemented.');
10
+ }
5
11
  find(id) {
6
12
  throw new Error('Method not implemented.');
7
13
  }
@@ -23,6 +29,9 @@ let JobRepository = class JobRepository {
23
29
  delete(item) {
24
30
  throw new Error('Method not implemented.');
25
31
  }
32
+ countRunningByCommand(commandName) {
33
+ throw new Error('Method not implemented.');
34
+ }
26
35
  };
27
36
  JobRepository = __decorate([
28
37
  singleton()
@@ -1,8 +1,10 @@
1
1
  import { __decorate, __metadata } from 'tslib';
2
- import { CommandMetadataStore } from './CommandMetadataStore.js';
2
+ import { AsyncMetadataStore } from './AsyncMetadataStore.js';
3
3
  import { JobRepository } from './JobRepository.js';
4
4
  import { singleton, container } from '../../core/injection/index.js';
5
5
  import { Logger } from '../../core/logger/Logger.js';
6
+ import '../../core/validation/metadata/ValidationMetadataStore.js';
7
+ import { validateAndTransform } from '../../core/validation/validateAndTransform.js';
6
8
 
7
9
  let JobRunner = class JobRunner {
8
10
  jobRepository;
@@ -14,20 +16,29 @@ let JobRunner = class JobRunner {
14
16
  }
15
17
  async run(job) {
16
18
  try {
17
- const { commandName, commandData } = job['data'];
19
+ const { commandName, commandData } = job;
18
20
  const handlerConstructor = this.handlerContainer.getHandlerForCommandName(commandName);
19
21
  if (!handlerConstructor) {
20
22
  throw new Error(`Not found handler for command '${commandName}'`);
21
23
  }
22
24
  const handler = container.resolve(handlerConstructor);
23
25
  const commandConstructor = this.handlerContainer.getCommandForCommandName(commandName);
24
- if (!commandConstructor) {
25
- throw new Error(`Not found class for command name '${commandName}'`);
26
+ if (commandConstructor === undefined && commandData != undefined) {
27
+ throw new Error(`Not found class for validate data of command '${commandName}'`);
26
28
  }
27
29
  job.setAsStarted();
28
30
  await this.jobRepository.update(job);
29
- const command = new commandConstructor(commandData);
31
+ let command = undefined;
32
+ if (commandConstructor) {
33
+ const validationResult = validateAndTransform(commandData, commandConstructor);
34
+ if (!validationResult.value) {
35
+ throw new Error('Invalid command data');
36
+ }
37
+ command = validationResult.value;
38
+ }
39
+ this.logger.debug(`start running command ${commandName}`);
30
40
  await handler.handle(command);
41
+ this.logger.debug(`command ${commandName} run successfull`);
31
42
  job.setAsSuccess();
32
43
  }
33
44
  catch (e) {
@@ -42,7 +53,7 @@ let JobRunner = class JobRunner {
42
53
  JobRunner = __decorate([
43
54
  singleton(),
44
55
  __metadata("design:paramtypes", [JobRepository,
45
- CommandMetadataStore])
56
+ AsyncMetadataStore])
46
57
  ], JobRunner);
47
58
 
48
59
  export { JobRunner };