@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.
- package/README.md +9 -12
- package/dist/src/addon/async/pg/PgJobRepository.js +50 -2
- package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +5 -0
- package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +7 -2
- package/dist/src/addon/chat-bot/openia/OpenaiChatAdapter.js +15 -3
- package/dist/src/addon/chat-bot/pg/PgChatMemory.js +5 -0
- package/dist/src/addon/chat-bot/pg/PgChatRepository.js +5 -0
- package/dist/src/addon/chat-bot/wabot/WabotChatAdapter.js +1 -1
- package/dist/src/addon/chat-controller/whatsapp/PgWhatsAppRepository.js +5 -0
- package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppReceiverByWabotProxy.js +1 -0
- package/dist/src/core/lock/Locker.js +7 -0
- package/dist/src/feature/async/@command.js +3 -3
- package/dist/src/feature/async/@commandHandler.js +3 -3
- package/dist/src/feature/async/Async.js +21 -14
- package/dist/src/feature/async/{CommandMetadataStore.js → AsyncMetadataStore.js} +27 -5
- package/dist/src/feature/async/Job.js +92 -2
- package/dist/src/feature/async/JobExecutor.js +62 -0
- package/dist/src/feature/async/JobRepository.js +9 -0
- package/dist/src/feature/async/JobRunner.js +17 -6
- package/dist/src/feature/async/JobScheduler.js +80 -0
- package/dist/src/feature/async/JobWatchdog.js +74 -0
- package/dist/src/feature/async/runCommandHandlers.js +19 -25
- package/dist/src/feature/chat-bot/Chat.js +3 -0
- package/dist/src/feature/chat-controller/runChatControllers.js +1 -1
- package/dist/src/feature/pg/PgCrudRepository.js +7 -1
- package/dist/src/feature/pg/PgLock.js +20 -0
- package/dist/src/feature/pg/PgLockKey.js +68 -0
- package/dist/src/feature/pg/PgRepositoryBase.js +40 -27
- package/dist/src/feature/pg/pgStorage.js +16 -0
- package/dist/src/feature/pg/withPgClient.js +45 -0
- package/dist/src/feature/pg/withPgTransaction.js +45 -0
- package/dist/src/index.d.ts +252 -96
- package/dist/src/index.js +8 -4
- package/package.json +8 -6
- package/dist/src/feature/async/Command.js +0 -9
- 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/)
|
|
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
|
-
|
|
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:** [
|
|
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) • [
|
|
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/
|
|
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/
|
|
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 {
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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-
|
|
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
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
7
|
-
|
|
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 {
|
|
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
|
|
7
|
-
|
|
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 {
|
|
3
|
+
import { AsyncMetadataStore } from './AsyncMetadataStore.js';
|
|
4
4
|
import { Job } from './Job.js';
|
|
5
5
|
import { JobRepository } from './JobRepository.js';
|
|
6
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
constructor(jobRepository,
|
|
12
|
+
metadataStore;
|
|
13
|
+
jobScheduler;
|
|
14
|
+
constructor(jobRepository, metadataStore, jobScheduler) {
|
|
13
15
|
this.jobRepository = jobRepository;
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
+
this.metadataStore = metadataStore;
|
|
17
|
+
this.jobScheduler = jobScheduler;
|
|
16
18
|
}
|
|
17
|
-
async
|
|
18
|
-
const commandName = this.
|
|
19
|
+
async runCommand(ctor, data) {
|
|
20
|
+
const commandName = this.metadataStore.getCommandName(ctor);
|
|
19
21
|
if (!commandName) {
|
|
20
|
-
throw new Error(`${
|
|
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
|
|
30
|
+
commandData,
|
|
31
|
+
scheduledAt: new Date().getTime(),
|
|
25
32
|
});
|
|
26
33
|
await this.jobRepository.create(job);
|
|
27
|
-
this.
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
AsyncMetadataStore = __decorate([
|
|
35
57
|
singleton()
|
|
36
|
-
],
|
|
58
|
+
], AsyncMetadataStore);
|
|
37
59
|
|
|
38
|
-
export {
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 (
|
|
25
|
-
throw new Error(`Not found class for command
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
AsyncMetadataStore])
|
|
46
57
|
], JobRunner);
|
|
47
58
|
|
|
48
59
|
export { JobRunner };
|