Skip to content

Commit 0a6768c

Browse files
committed
Moved automations service to separate file
towards https://linear.app/ghost/issue/NY-1286 ref #28120 This change should have no impact on functionality. We have [a lint rule that limits the length of `index.js` files][0]. In [an upcoming change][1], the automations service will cross that limit. This patch moves it into a separate file. Doing so also makes it a little easier to test. [0]: https://github.com/TryGhost/eslint-plugin-ghost/blob/22d154a8e3807e4e30219fb8449e7aa9b90a110b/lib/config/node.js#L32-L37 [1]: #28120
1 parent 87afad8 commit 0a6768c

3 files changed

Lines changed: 92 additions & 92 deletions

File tree

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,3 @@
1-
// @ts-check
2-
const urlUtils = require('../../../shared/url-utils');
3-
const {oneAtATime} = require('../../../shared/one-at-a-time');
4-
const logging = require('@tryghost/logging');
5-
const {getSignedAdminToken} = require('../../adapters/scheduling/utils');
6-
const StartAutomationsPollEvent = require('./events/start-automations-poll-event');
7-
const {poll} = require('./poll');
8-
const {welcomeEmailAutomationPoll} = require('./welcome-email-automation-poll');
9-
const memberWelcomeEmailService = require('../member-welcome-emails/service');
10-
/** @import DomainEvents from '@tryghost/domain-events' */
11-
12-
/**
13-
* @internal
14-
* @typedef {object} SchedulerAdapter
15-
* @prop {(job: {
16-
* time: number;
17-
* url: string;
18-
* extra: {
19-
* httpMethod: string;
20-
* };
21-
* }) => void} schedule
22-
* @prop {(rescheduler: {rescheduleAll: () => unknown}) => void} register
23-
*/
24-
25-
class AutomationsService {
26-
#initialized = false;
27-
#enqueuePollNow;
28-
29-
/**
30-
* @param {object} options
31-
* @param {Pick<DomainEvents, 'dispatch' | 'subscribe'>} options.domainEvents
32-
* @param {string} options.apiUrl
33-
* @param {SchedulerAdapter} options.schedulerAdapter
34-
* @param {ReadonlyMap<string, Promise<{id: string, secret: string}>>} options.internalKeys
35-
* @returns {void}
36-
*/
37-
init({domainEvents, apiUrl, schedulerAdapter, internalKeys}) {
38-
if (this.#initialized) {
39-
return;
40-
}
41-
42-
this.#enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());
43-
44-
/** @param {Readonly<Date>} date */
45-
const enqueuePollAt = async (date) => {
46-
const isRequestedDateInTheFuture = new Date() < date;
47-
if (!isRequestedDateInTheFuture) {
48-
this.#enqueuePollNow();
49-
return;
50-
}
51-
52-
try {
53-
const key = await internalKeys.get('ghost-scheduler');
54-
const signedAdminToken = getSignedAdminToken({publishedAt: date.toISOString(), apiUrl, key});
55-
const url = new URL(urlUtils.urlJoin(apiUrl, 'automations', 'poll'));
56-
url.searchParams.set('token', signedAdminToken);
57-
schedulerAdapter.schedule({time: date.getTime(), url: url.toString(), extra: {httpMethod: 'PUT'}});
58-
} catch (err) {
59-
logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
60-
}
61-
};
62-
63-
domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
64-
enqueueAnotherPollAt: enqueuePollAt
65-
})));
66-
67-
domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => welcomeEmailAutomationPoll({
68-
memberWelcomeEmailService,
69-
enqueueAnotherPollAt: enqueuePollAt
70-
})));
71-
72-
schedulerAdapter.register(this);
73-
74-
enqueuePollAt(new Date());
75-
76-
this.#initialized = true;
77-
}
78-
79-
/**
80-
* Re-arm the poll chain. A queued poll signed under the previous scheduler
81-
* key fails JWT verification when fired; this dispatches a fresh in-process
82-
* poll that re-schedules the next callback under the current key.
83-
*/
84-
rescheduleAll() {
85-
this.#enqueuePollNow?.();
86-
}
87-
}
1+
const AutomationsService = require('./service');
882

893
module.exports = new AutomationsService();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// @ts-check
2+
const urlUtils = require('../../../shared/url-utils');
3+
const {oneAtATime} = require('../../../shared/one-at-a-time');
4+
const logging = require('@tryghost/logging');
5+
const {getSignedAdminToken} = require('../../adapters/scheduling/utils');
6+
const StartAutomationsPollEvent = require('./events/start-automations-poll-event');
7+
const {poll} = require('./poll');
8+
const {welcomeEmailAutomationPoll} = require('./welcome-email-automation-poll');
9+
const memberWelcomeEmailService = require('../member-welcome-emails/service');
10+
/** @import DomainEvents from '@tryghost/domain-events' */
11+
12+
/**
13+
* @internal
14+
* @typedef {object} SchedulerAdapter
15+
* @prop {(job: {
16+
* time: number;
17+
* url: string;
18+
* extra: {
19+
* httpMethod: string;
20+
* };
21+
* }) => void} schedule
22+
* @prop {(rescheduler: {rescheduleAll: () => unknown}) => void} register
23+
*/
24+
25+
class AutomationsService {
26+
#initialized = false;
27+
#enqueuePollNow;
28+
29+
/**
30+
* @param {object} options
31+
* @param {Pick<DomainEvents, 'dispatch' | 'subscribe'>} options.domainEvents
32+
* @param {string} options.apiUrl
33+
* @param {SchedulerAdapter} options.schedulerAdapter
34+
* @param {ReadonlyMap<string, Promise<{id: string, secret: string}>>} options.internalKeys
35+
* @returns {void}
36+
*/
37+
init({domainEvents, apiUrl, schedulerAdapter, internalKeys}) {
38+
if (this.#initialized) {
39+
return;
40+
}
41+
42+
this.#enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());
43+
44+
/** @param {Readonly<Date>} date */
45+
const enqueuePollAt = async (date) => {
46+
const isRequestedDateInTheFuture = new Date() < date;
47+
if (!isRequestedDateInTheFuture) {
48+
this.#enqueuePollNow();
49+
return;
50+
}
51+
52+
try {
53+
const key = await internalKeys.get('ghost-scheduler');
54+
const signedAdminToken = getSignedAdminToken({publishedAt: date.toISOString(), apiUrl, key});
55+
const url = new URL(urlUtils.urlJoin(apiUrl, 'automations', 'poll'));
56+
url.searchParams.set('token', signedAdminToken);
57+
schedulerAdapter.schedule({time: date.getTime(), url: url.toString(), extra: {httpMethod: 'PUT'}});
58+
} catch (err) {
59+
logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
60+
}
61+
};
62+
63+
domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
64+
enqueueAnotherPollAt: enqueuePollAt
65+
})));
66+
67+
domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => welcomeEmailAutomationPoll({
68+
memberWelcomeEmailService,
69+
enqueueAnotherPollAt: enqueuePollAt
70+
})));
71+
72+
schedulerAdapter.register(this);
73+
74+
enqueuePollAt(new Date());
75+
76+
this.#initialized = true;
77+
}
78+
79+
/**
80+
* Re-arm the poll chain. A queued poll signed under the previous scheduler
81+
* key fails JWT verification when fired; this dispatches a fresh in-process
82+
* poll that re-schedules the next callback under the current key.
83+
*/
84+
rescheduleAll() {
85+
this.#enqueuePollNow?.();
86+
}
87+
}
88+
89+
module.exports = AutomationsService;

ghost/core/test/unit/server/services/automations/index.test.js renamed to ghost/core/test/unit/server/services/automations/service.test.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
const sinon = require('sinon');
22

33
const StartAutomationsPollEvent = require('../../../../../core/server/services/automations/events/start-automations-poll-event');
4-
5-
const automationsModulePath = require.resolve('../../../../../core/server/services/automations');
4+
const AutomationsService = require('../../../../../core/server/services/automations').constructor;
65

76
describe('automations service', function () {
87
let automations;
@@ -11,9 +10,7 @@ describe('automations service', function () {
1110
let initOptions;
1211

1312
beforeEach(function () {
14-
// Reset the module-level singleton between tests.
15-
delete require.cache[automationsModulePath];
16-
automations = require(automationsModulePath);
13+
automations = new AutomationsService();
1714
domainEvents = {
1815
dispatch: sinon.stub(),
1916
subscribe: sinon.stub()

0 commit comments

Comments
 (0)