@wabot-dev/framework 0.3.0 → 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/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/pg/PgChatMemory.js +5 -0
- package/dist/src/addon/chat-bot/pg/PgChatRepository.js +5 -0
- 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-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 +239 -99
- package/dist/src/index.js +8 -4
- package/package.json +7 -5
- package/dist/src/feature/async/Command.js +0 -9
- package/dist/src/feature/async/JobsEventsHub.js +0 -36
|
@@ -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) {
|
|
@@ -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';
|
|
@@ -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 };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { __decorate, __metadata } from 'tslib';
|
|
2
|
+
import { singleton } from '../../core/injection/index.js';
|
|
3
|
+
import { JobRepository } from './JobRepository.js';
|
|
4
|
+
import { Env } from '../../core/env/Env.js';
|
|
5
|
+
import { Logger } from '../../core/logger/Logger.js';
|
|
6
|
+
import { JobExecutor } from './JobExecutor.js';
|
|
7
|
+
import { Locker } from '../../core/lock/Locker.js';
|
|
8
|
+
|
|
9
|
+
let JobScheduler = class JobScheduler {
|
|
10
|
+
locker;
|
|
11
|
+
repo;
|
|
12
|
+
executor;
|
|
13
|
+
env;
|
|
14
|
+
timeout;
|
|
15
|
+
logger = new Logger('wabot:job-scheduler');
|
|
16
|
+
commands = new Set();
|
|
17
|
+
running = false;
|
|
18
|
+
constructor(locker, repo, executor, env) {
|
|
19
|
+
this.locker = locker;
|
|
20
|
+
this.repo = repo;
|
|
21
|
+
this.executor = executor;
|
|
22
|
+
this.env = env;
|
|
23
|
+
}
|
|
24
|
+
start(commands) {
|
|
25
|
+
this.logger.info(`starting handlers for commands ${commands}`);
|
|
26
|
+
commands.forEach((x) => this.commands.add(x));
|
|
27
|
+
this.logger.info(`starting job handlers for commands ${commands.join(', ')}`);
|
|
28
|
+
if (this.commands.size > 0 && !this.running) {
|
|
29
|
+
this.running = true;
|
|
30
|
+
this.tick();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
stop(commands) {
|
|
34
|
+
this.logger.info(`stoping handlers for commands ${commands}`);
|
|
35
|
+
commands.forEach((x) => this.commands.delete(x));
|
|
36
|
+
if (this.commands.size === 0) {
|
|
37
|
+
this.running = false;
|
|
38
|
+
if (this.timeout) {
|
|
39
|
+
clearTimeout(this.timeout);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
tryExecuteNow(job) {
|
|
44
|
+
if (this.commands.has(job.commandName))
|
|
45
|
+
this.executor.execute(job).catch((e) => this.logger.error(e));
|
|
46
|
+
}
|
|
47
|
+
async tick() {
|
|
48
|
+
if (!this.running)
|
|
49
|
+
return;
|
|
50
|
+
const interval = this.env.requireNumber('WABOT_JOB_SCHEDULER_INTERVAL_SECONDS', {
|
|
51
|
+
default: 10,
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const remainingSlots = this.executor.remainingSlots();
|
|
55
|
+
if (remainingSlots === 0)
|
|
56
|
+
return;
|
|
57
|
+
await this.locker.withKey('wabot-job-scheduler-loop').run(async () => {
|
|
58
|
+
const jobs = await this.repo.findPendingForRunFrom(new Date(), this.executor.remainingSlots());
|
|
59
|
+
const readyToRunJobs = jobs.filter((job) => this.commands.has(job.commandName) && job.isScheduleReady());
|
|
60
|
+
readyToRunJobs.forEach((j) => this.executor.execute(j).catch((e) => this.logger.error(e)));
|
|
61
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
this.logger.error(e);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
this.timeout = setTimeout(() => this.tick(), interval * 1000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
JobScheduler = __decorate([
|
|
73
|
+
singleton(),
|
|
74
|
+
__metadata("design:paramtypes", [Locker,
|
|
75
|
+
JobRepository,
|
|
76
|
+
JobExecutor,
|
|
77
|
+
Env])
|
|
78
|
+
], JobScheduler);
|
|
79
|
+
|
|
80
|
+
export { JobScheduler };
|