@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.
Files changed (32) hide show
  1. package/dist/src/addon/async/pg/PgJobRepository.js +50 -2
  2. package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +5 -0
  3. package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +7 -2
  4. package/dist/src/addon/chat-bot/pg/PgChatMemory.js +5 -0
  5. package/dist/src/addon/chat-bot/pg/PgChatRepository.js +5 -0
  6. package/dist/src/addon/chat-controller/whatsapp/PgWhatsAppRepository.js +5 -0
  7. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppReceiverByWabotProxy.js +1 -0
  8. package/dist/src/core/lock/Locker.js +7 -0
  9. package/dist/src/feature/async/@command.js +3 -3
  10. package/dist/src/feature/async/@commandHandler.js +3 -3
  11. package/dist/src/feature/async/Async.js +21 -14
  12. package/dist/src/feature/async/{CommandMetadataStore.js → AsyncMetadataStore.js} +27 -5
  13. package/dist/src/feature/async/Job.js +92 -2
  14. package/dist/src/feature/async/JobExecutor.js +62 -0
  15. package/dist/src/feature/async/JobRepository.js +9 -0
  16. package/dist/src/feature/async/JobRunner.js +17 -6
  17. package/dist/src/feature/async/JobScheduler.js +80 -0
  18. package/dist/src/feature/async/JobWatchdog.js +74 -0
  19. package/dist/src/feature/async/runCommandHandlers.js +19 -25
  20. package/dist/src/feature/chat-controller/runChatControllers.js +1 -1
  21. package/dist/src/feature/pg/PgCrudRepository.js +7 -1
  22. package/dist/src/feature/pg/PgLock.js +20 -0
  23. package/dist/src/feature/pg/PgLockKey.js +68 -0
  24. package/dist/src/feature/pg/PgRepositoryBase.js +40 -27
  25. package/dist/src/feature/pg/pgStorage.js +16 -0
  26. package/dist/src/feature/pg/withPgClient.js +45 -0
  27. package/dist/src/feature/pg/withPgTransaction.js +45 -0
  28. package/dist/src/index.d.ts +239 -99
  29. package/dist/src/index.js +8 -4
  30. package/package.json +7 -5
  31. package/dist/src/feature/async/Command.js +0 -9
  32. 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/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) {
@@ -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
  },
@@ -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 };
@@ -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 };