@tsed/pulse 8.0.1 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,13 +2,14 @@
2
2
  "name": "@tsed/pulse",
3
3
  "description": "@pulsecron/pulse package for Ts.ED framework",
4
4
  "type": "module",
5
- "version": "8.0.1",
5
+ "version": "8.0.3",
6
6
  "source": "./src/index.ts",
7
7
  "main": "./lib/esm/index.js",
8
8
  "module": "./lib/esm/index.js",
9
9
  "typings": "./lib/types/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
+ "@tsed/source": "./src/index.ts",
12
13
  "types": "./lib/types/index.d.ts",
13
14
  "import": "./lib/esm/index.js",
14
15
  "default": "./lib/esm/index.js"
@@ -26,11 +27,11 @@
26
27
  },
27
28
  "devDependencies": {
28
29
  "@pulsecron/pulse": "^1.6.3",
29
- "@tsed/barrels": "8.0.1",
30
- "@tsed/core": "8.0.1",
31
- "@tsed/di": "8.0.1",
32
- "@tsed/platform-http": "8.0.1",
33
- "@tsed/typescript": "8.0.1",
30
+ "@tsed/barrels": "8.0.3",
31
+ "@tsed/core": "8.0.3",
32
+ "@tsed/di": "8.0.3",
33
+ "@tsed/platform-http": "8.0.3",
34
+ "@tsed/typescript": "8.0.3",
34
35
  "eslint": "9.12.0",
35
36
  "typescript": "5.4.5",
36
37
  "vitest": "2.1.2"
@@ -0,0 +1,221 @@
1
+ import type {Job} from "@pulsecron/pulse";
2
+ import {Inject} from "@tsed/di";
3
+ import {PlatformTest} from "@tsed/platform-http/testing";
4
+
5
+ import {Define} from "./decorators/define.js";
6
+ import {Every} from "./decorators/every.js";
7
+ import {Pulse} from "./decorators/pulse.js";
8
+ import {PulseModule} from "./PulseModule.js";
9
+
10
+ vi.mock("@pulsecron/pulse", () => {
11
+ return {
12
+ Pulse: class {
13
+ close = vi.fn();
14
+ stop = vi.fn();
15
+ drain = vi.fn();
16
+ define = vi.fn();
17
+ every = vi.fn();
18
+ schedule = vi.fn();
19
+ now = vi.fn();
20
+ create = vi.fn().mockReturnValue({
21
+ repeatEvery: vi.fn().mockReturnThis(),
22
+ save: vi.fn()
23
+ });
24
+ start = vi.fn();
25
+ }
26
+ };
27
+ });
28
+
29
+ @Pulse({namespace: "test-nsp"})
30
+ class CustomCampaign {
31
+ @Inject()
32
+ pulse: PulseModule;
33
+
34
+ job: Job;
35
+
36
+ $beforePulseStart() {
37
+ this.job = this.pulse.create("customName2", {
38
+ locale: "fr-FR"
39
+ });
40
+ }
41
+
42
+ $afterPulseStart() {
43
+ return this.job.repeatEvery("1 week").save();
44
+ }
45
+
46
+ @Every("60 seconds")
47
+ test(job: Job) {
48
+ // test
49
+ return "hello " + job.attrs.name;
50
+ }
51
+
52
+ @Define({name: "customName"})
53
+ test2(job: Job) {
54
+ // test
55
+ return "hello " + job.attrs.name;
56
+ }
57
+
58
+ @Define({name: "customName2"})
59
+ test3() {
60
+ // test
61
+ }
62
+ }
63
+
64
+ @Pulse({namespace: "test-nsp-2"})
65
+ class CustomCampaign2 {
66
+ @Define({name: "customName"})
67
+ test(job: Job) {
68
+ // test
69
+ return "hello " + job.attrs.name;
70
+ }
71
+ }
72
+
73
+ @Pulse({namespace: "test-nsp-3"})
74
+ class CustomCampaign3 {}
75
+
76
+ describe("PulseModule", () => {
77
+ describe("when pulse is enabled", () => {
78
+ beforeEach(() =>
79
+ PlatformTest.create({
80
+ pulse: {
81
+ enabled: true
82
+ }
83
+ })
84
+ );
85
+ afterEach(() => PlatformTest.reset());
86
+
87
+ describe("$afterListen()", () => {
88
+ it("should load all jobs", async () => {
89
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
90
+ const campaign = PlatformTest.get<CustomCampaign>(CustomCampaign)!;
91
+
92
+ await pulseModule.$afterListen();
93
+
94
+ expect(pulseModule.pulse.define).toHaveBeenCalledWith("test-nsp.test", expect.any(Function), {});
95
+ expect(pulseModule.pulse.every).toHaveBeenCalledWith("60 seconds", "test-nsp.test", {}, {});
96
+ expect(pulseModule.pulse.start).toHaveBeenCalledWith();
97
+ expect(pulseModule.pulse.create).toHaveBeenCalledWith("customName2", {locale: "fr-FR"});
98
+
99
+ expect(campaign.job.repeatEvery).toHaveBeenCalledWith("1 week");
100
+ expect(campaign.job.save).toHaveBeenCalledWith();
101
+
102
+ const result = await pulseModule.pulse.define.mock.calls[0][1]({
103
+ attrs: {
104
+ name: "test-nsp.test"
105
+ }
106
+ });
107
+
108
+ expect(result).toEqual("hello test-nsp.test");
109
+ });
110
+ });
111
+ describe("schedule()", () => {
112
+ it("should schedule a job", async () => {
113
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
114
+
115
+ await pulseModule.schedule("now", "test-nsp.test", {});
116
+
117
+ expect(pulseModule.pulse.schedule).toHaveBeenCalledWith("now", "test-nsp.test", {});
118
+ });
119
+ });
120
+
121
+ describe("now()", () => {
122
+ it("should schedule a job", async () => {
123
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
124
+
125
+ await pulseModule.now("test-nsp.test", {});
126
+
127
+ expect(pulseModule.pulse.now).toHaveBeenCalledWith("test-nsp.test", {});
128
+ });
129
+ });
130
+ describe("$onDestroy()", () => {
131
+ it("should close pulse", async () => {
132
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
133
+
134
+ await pulseModule.$onDestroy();
135
+
136
+ expect(pulseModule.pulse.stop).toHaveBeenCalledWith();
137
+ expect(pulseModule.pulse.close).toHaveBeenCalledWith({force: true});
138
+ });
139
+ });
140
+ });
141
+ describe("when pulse is enabled and drainJobsBeforeClose = true", () => {
142
+ beforeEach(() =>
143
+ PlatformTest.create({
144
+ pulse: {
145
+ enabled: true,
146
+ drainJobsBeforeClose: true
147
+ }
148
+ })
149
+ );
150
+ afterEach(() => PlatformTest.reset());
151
+ describe("$onDestroy()", () => {
152
+ it("should close pulse", async () => {
153
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
154
+
155
+ await pulseModule.$onDestroy();
156
+
157
+ expect(pulseModule.pulse.drain).toHaveBeenCalledWith();
158
+ expect(pulseModule.pulse.close).toHaveBeenCalledWith({force: true});
159
+ });
160
+ });
161
+ });
162
+ describe("when pulse is enabled but disableJobProcessing = true", () => {
163
+ beforeEach(() =>
164
+ PlatformTest.create({
165
+ pulse: {
166
+ enabled: true,
167
+ disableJobProcessing: true
168
+ }
169
+ })
170
+ );
171
+ afterEach(() => PlatformTest.reset());
172
+
173
+ describe("$afterListen()", () => {
174
+ it("should load all jobs", async () => {
175
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
176
+ const campaign = PlatformTest.get<CustomCampaign>(CustomCampaign)!;
177
+
178
+ await pulseModule.$afterListen();
179
+
180
+ expect(pulseModule.pulse.define).not.toHaveBeenCalled();
181
+ expect(pulseModule.pulse.every).not.toHaveBeenCalled();
182
+ expect(pulseModule.pulse.start).toHaveBeenCalledWith();
183
+ expect(pulseModule.pulse.create).not.toHaveBeenCalled();
184
+
185
+ expect(campaign.job).toBeUndefined();
186
+ });
187
+ });
188
+ });
189
+
190
+ describe("when pulse is disabled", () => {
191
+ beforeEach(() =>
192
+ PlatformTest.create({
193
+ pulse: {
194
+ enabled: false
195
+ }
196
+ })
197
+ );
198
+ afterEach(() => PlatformTest.reset());
199
+
200
+ describe("$afterListen()", () => {
201
+ it("should load all jobs", async () => {
202
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
203
+ const campaign = PlatformTest.get<CustomCampaign>(CustomCampaign)!;
204
+
205
+ await pulseModule.$afterListen();
206
+
207
+ expect(pulseModule.pulse.define).toBeUndefined();
208
+
209
+ expect(campaign.job).toBeUndefined();
210
+ });
211
+ });
212
+
213
+ describe("$onDestroy()", () => {
214
+ it("should do nothing", async () => {
215
+ const pulseModule = PlatformTest.get<any>(PulseModule)!;
216
+
217
+ await pulseModule.$onDestroy();
218
+ });
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,156 @@
1
+ import {DefineOptions, Job, JobAttributesData, Processor} from "@pulsecron/pulse";
2
+ import {Constant, DIContext, Inject, InjectorService, Module, OnDestroy, Provider, runInContext} from "@tsed/di";
3
+ import {Logger} from "@tsed/logger";
4
+ import {AfterListen} from "@tsed/platform-http";
5
+ import {v4 as uuid} from "uuid";
6
+
7
+ import {PROVIDER_TYPE_PULSE} from "./constants/constants.js";
8
+ import {PulseStore} from "./interfaces/PulseStore.js";
9
+ import {PulseService} from "./services/PulseFactory.js";
10
+
11
+ @Module()
12
+ export class PulseModule implements OnDestroy, AfterListen {
13
+ @Inject()
14
+ protected logger: Logger;
15
+
16
+ @Inject()
17
+ protected injector: InjectorService;
18
+
19
+ @Inject()
20
+ protected pulse: PulseService;
21
+
22
+ @Constant("pulse.enabled", false)
23
+ private enabled: boolean;
24
+
25
+ @Constant("pulse.drainJobsBeforeClose", false)
26
+ private drainJobsBeforeClose: boolean;
27
+
28
+ @Constant("pulse.disableJobProcessing", false)
29
+ private disableJobProcessing: boolean;
30
+
31
+ async $afterListen(): Promise<any> {
32
+ if (this.enabled) {
33
+ const providers = this.getProviders();
34
+
35
+ if (!this.disableJobProcessing) {
36
+ this.logger.info({
37
+ event: "PULSE_ADD_DEFINITIONS",
38
+ message: "Pulse add definitions"
39
+ });
40
+ providers.forEach((provider) => this.addPulseDefinitionsForProvider(provider));
41
+
42
+ await this.injector.emit("$beforePulseStart");
43
+ }
44
+
45
+ await this.pulse.start();
46
+
47
+ if (!this.disableJobProcessing) {
48
+ this.logger.info({
49
+ event: "PULSE_ADD_JOBS",
50
+ message: "Pulse add scheduled jobs"
51
+ });
52
+ await Promise.all(providers.map((provider) => this.scheduleJobsForProvider(provider)));
53
+
54
+ await this.injector.emit("$afterPulseStart");
55
+ }
56
+ }
57
+ }
58
+
59
+ async $onDestroy(): Promise<any> {
60
+ if (this.enabled) {
61
+ if (this.drainJobsBeforeClose && "drain" in this.pulse) {
62
+ this.logger.info({
63
+ event: "PULSE_DRAIN",
64
+ message: "Pulse is draining all jobs before close"
65
+ });
66
+ await this.pulse.drain();
67
+ } else {
68
+ this.logger.info({
69
+ event: "PULSE_STOP",
70
+ message: "Pulse is stopping"
71
+ });
72
+ await this.pulse.stop();
73
+ }
74
+
75
+ await this.pulse.close({force: true});
76
+
77
+ this.logger.info({event: "PULSE_STOP", message: "Pulse stopped"});
78
+ }
79
+ }
80
+
81
+ define<T extends JobAttributesData>(name: string, processor: Processor<T>, options?: DefineOptions) {
82
+ return this.pulse.define(
83
+ name,
84
+ (job: Job<T>, ...rest) => {
85
+ const $ctx = new DIContext({
86
+ injector: this.injector,
87
+ id: uuid(),
88
+ logger: this.injector.logger
89
+ });
90
+
91
+ $ctx.set("job", job);
92
+
93
+ return runInContext($ctx, () => processor(job, ...rest));
94
+ },
95
+ options
96
+ );
97
+ }
98
+
99
+ every(interval: string, name: string, data?: any, options?: any) {
100
+ return this.pulse.every(interval, name, data, options);
101
+ }
102
+
103
+ schedule(when: Date | string, name: string, data?: any) {
104
+ return this.pulse.schedule(when, name, data);
105
+ }
106
+
107
+ now(name: string, data?: any) {
108
+ return this.pulse.now(name, data);
109
+ }
110
+
111
+ create(name: string, data?: any) {
112
+ return this.pulse.create(name, data);
113
+ }
114
+
115
+ protected getProviders(): Provider<any>[] {
116
+ return this.injector.getProviders(PROVIDER_TYPE_PULSE);
117
+ }
118
+
119
+ protected addPulseDefinitionsForProvider(provider: Provider): void {
120
+ const store = provider.store.get<PulseStore>("pulse", {});
121
+
122
+ if (!store.define) {
123
+ return;
124
+ }
125
+
126
+ Object.entries(store.define).forEach(([propertyKey, {name, ...options}]) => {
127
+ const instance = this.injector.get(provider.token);
128
+
129
+ const jobProcessor: Processor<never> = instance[propertyKey].bind(instance) as Processor<never>;
130
+ const jobName = this.getNameForJob(propertyKey, store.namespace, name);
131
+
132
+ this.define(jobName, jobProcessor, options);
133
+ });
134
+ }
135
+
136
+ protected async scheduleJobsForProvider(provider: Provider<any>): Promise<void> {
137
+ const store = provider.store.get<PulseStore>("pulse", {});
138
+
139
+ if (!store.every) {
140
+ return;
141
+ }
142
+
143
+ const promises = Object.entries(store.every).map(([propertyKey, {interval, name, ...options}]) => {
144
+ const jobName = this.getNameForJob(propertyKey, store.namespace, name);
145
+
146
+ return this.every(interval, jobName, {}, options);
147
+ });
148
+
149
+ await Promise.all(promises);
150
+ }
151
+
152
+ protected getNameForJob(propertyKey: string, namespace?: string, customName?: string): string {
153
+ const name = customName || propertyKey;
154
+ return namespace ? `${namespace}.${name}` : name;
155
+ }
156
+ }
@@ -0,0 +1 @@
1
+ export const PROVIDER_TYPE_PULSE = "PROVIDER_TYPE_PULSE";
@@ -0,0 +1,49 @@
1
+ import {Store} from "@tsed/core";
2
+
3
+ import {Define} from "./define.js";
4
+ import {Pulse} from "./pulse.js";
5
+
6
+ describe("@Define()", () => {
7
+ it("should set empty metadata", () => {
8
+ @Pulse()
9
+ class Test {
10
+ @Define()
11
+ testDefineDecorator() {
12
+ // test
13
+ }
14
+
15
+ @Define()
16
+ testDefineDecorator2() {
17
+ // test 2
18
+ }
19
+ }
20
+
21
+ const store = Store.from(Test);
22
+
23
+ expect(store.get("pulse").define).toEqual({
24
+ testDefineDecorator: {},
25
+ testDefineDecorator2: {}
26
+ });
27
+ });
28
+
29
+ it("should set options metadata", () => {
30
+ @Pulse()
31
+ class Test {
32
+ @Define({
33
+ name: "testDefineDecoratorCustomName",
34
+ priority: "highest"
35
+ })
36
+ test() {
37
+ // test
38
+ }
39
+ }
40
+
41
+ const store = Store.from(Test);
42
+ expect(store.get("pulse").define).toEqual({
43
+ test: {
44
+ name: "testDefineDecoratorCustomName",
45
+ priority: "highest"
46
+ }
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,15 @@
1
+ import {Store} from "@tsed/core";
2
+
3
+ import {DefineOptions, PulseStore} from "../interfaces/PulseStore.js";
4
+
5
+ export function Define(options: DefineOptions = {}): MethodDecorator {
6
+ return (target: any, propertyKey: string) => {
7
+ const store: PulseStore = {
8
+ define: {
9
+ [propertyKey]: options
10
+ }
11
+ };
12
+
13
+ Store.from(target).merge("pulse", store);
14
+ };
15
+ }
@@ -0,0 +1,37 @@
1
+ import {Store} from "@tsed/core";
2
+
3
+ import {Every} from "./every.js";
4
+ import {Pulse} from "./pulse.js";
5
+
6
+ describe("@Every()", () => {
7
+ it("should set metadata", () => {
8
+ @Pulse()
9
+ class Test {
10
+ @Every("60 seconds")
11
+ testEveryDecorator() {
12
+ // test
13
+ }
14
+
15
+ @Every("* * * * *")
16
+ testEveryDecorator2() {
17
+ // test 2
18
+ }
19
+ }
20
+
21
+ const store = Store.from(Test);
22
+
23
+ expect(store.get("pulse").define).toEqual({
24
+ testEveryDecorator: {},
25
+ testEveryDecorator2: {}
26
+ });
27
+
28
+ expect(store.get("pulse").every).toEqual({
29
+ testEveryDecorator: {
30
+ interval: "60 seconds"
31
+ },
32
+ testEveryDecorator2: {
33
+ interval: "* * * * *"
34
+ }
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,15 @@
1
+ import {Store, useDecorators} from "@tsed/core";
2
+
3
+ import {EveryOptions, PulseStore} from "../interfaces/PulseStore.js";
4
+ import {Define} from "./define.js";
5
+
6
+ export function Every(interval: string, options: EveryOptions = {}): MethodDecorator {
7
+ return useDecorators(Define(options), (target: any, propertyKey: string) => {
8
+ const store: PulseStore = {
9
+ every: {
10
+ [propertyKey]: {...options, interval}
11
+ }
12
+ };
13
+ Store.from(target).merge("pulse", store);
14
+ });
15
+ }
@@ -0,0 +1,22 @@
1
+ import {Store} from "@tsed/core";
2
+
3
+ import {Pulse} from "./pulse.js";
4
+
5
+ describe("@Pulse()", () => {
6
+ it("should set empty metadata", () => {
7
+ @Pulse()
8
+ class Test {}
9
+
10
+ const store = Store.from(Test);
11
+ expect(store.get("pulse")).toEqual(undefined);
12
+ });
13
+ it("should set namespace metadata", () => {
14
+ @Pulse({namespace: "nsp1"})
15
+ class Test {}
16
+
17
+ const store = Store.from(Test);
18
+ expect(store.get("pulse")).toEqual({
19
+ namespace: "nsp1"
20
+ });
21
+ });
22
+ });
@@ -0,0 +1,17 @@
1
+ import {StoreMerge, useDecorators} from "@tsed/core";
2
+ import {Injectable} from "@tsed/di";
3
+
4
+ import {PROVIDER_TYPE_PULSE} from "../constants/constants.js";
5
+
6
+ interface PulseOptions {
7
+ namespace?: string;
8
+ }
9
+
10
+ export function Pulse(options?: PulseOptions): ClassDecorator {
11
+ return useDecorators(
12
+ options?.namespace && StoreMerge("pulse", options),
13
+ Injectable({
14
+ type: PROVIDER_TYPE_PULSE
15
+ })
16
+ );
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./constants/constants.js";
5
+ export * from "./decorators/define.js";
6
+ export * from "./decorators/every.js";
7
+ export * from "./decorators/pulse.js";
8
+ export * from "./interfaces/interfaces.js";
9
+ export * from "./interfaces/PulseStore.js";
10
+ export * from "./PulseModule.js";
11
+ export * from "./services/PulseFactory.js";
@@ -0,0 +1,13 @@
1
+ import {DefineOptions as PulseDefineOptions, JobOptions} from "@pulsecron/pulse";
2
+
3
+ export interface DefineOptions extends PulseDefineOptions {
4
+ name?: string;
5
+ }
6
+
7
+ export type EveryOptions = JobOptions & DefineOptions;
8
+
9
+ export interface PulseStore {
10
+ namespace?: string;
11
+ define?: {[propertyKey: string]: DefineOptions};
12
+ every?: {[propertyKey: string]: EveryOptions & {interval: string}};
13
+ }
@@ -0,0 +1,16 @@
1
+ import {PulseConfig} from "@pulsecron/pulse";
2
+
3
+ declare global {
4
+ namespace TsED {
5
+ interface Configuration {
6
+ pulse?: {
7
+ /**
8
+ * Enable Pulse jobs. Default false.
9
+ */
10
+ enabled?: boolean;
11
+ disableJobProcessing?: boolean;
12
+ drainJobsBeforeClose?: boolean;
13
+ } & PulseConfig;
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import {Pulse, PulseConfig} from "@pulsecron/pulse";
2
+ import {Configuration, registerProvider} from "@tsed/di";
3
+
4
+ // create Alias to avoid confusing import between Pulse from "@pulsecron/pulse" and Pulse decorator
5
+ export const PulseService = Pulse;
6
+ export type PulseService = Pulse;
7
+
8
+ registerProvider({
9
+ token: Pulse,
10
+ deps: [Configuration],
11
+ useFactory(settings: Configuration) {
12
+ const opts = settings.get<PulseConfig & {enabled: boolean}>("pulse", {enabled: false});
13
+ return opts.enabled ? new Pulse(opts) : {};
14
+ }
15
+ });