Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 1 addition & 87 deletions ghost/core/core/server/services/automations/index.js
Original file line number Diff line number Diff line change
@@ -1,89 +1,3 @@
// @ts-check
const urlUtils = require('../../../shared/url-utils');
const {oneAtATime} = require('../../../shared/one-at-a-time');
const logging = require('@tryghost/logging');
const {getSignedAdminToken} = require('../../adapters/scheduling/utils');
const StartAutomationsPollEvent = require('./events/start-automations-poll-event');
const {poll} = require('./poll');
const {welcomeEmailAutomationPoll} = require('./welcome-email-automation-poll');
const memberWelcomeEmailService = require('../member-welcome-emails/service');
/** @import DomainEvents from '@tryghost/domain-events' */

/**
* @internal
* @typedef {object} SchedulerAdapter
* @prop {(job: {
* time: number;
* url: string;
* extra: {
* httpMethod: string;
* };
* }) => void} schedule
* @prop {(rescheduler: {rescheduleAll: () => unknown}) => void} register
*/

class AutomationsService {
#initialized = false;
#enqueuePollNow;

/**
* @param {object} options
* @param {Pick<DomainEvents, 'dispatch' | 'subscribe'>} options.domainEvents
* @param {string} options.apiUrl
* @param {SchedulerAdapter} options.schedulerAdapter
* @param {ReadonlyMap<string, Promise<{id: string, secret: string}>>} options.internalKeys
* @returns {void}
*/
init({domainEvents, apiUrl, schedulerAdapter, internalKeys}) {
if (this.#initialized) {
return;
}

this.#enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());

/** @param {Readonly<Date>} date */
const enqueuePollAt = async (date) => {
const isRequestedDateInTheFuture = new Date() < date;
if (!isRequestedDateInTheFuture) {
this.#enqueuePollNow();
return;
}

try {
const key = await internalKeys.get('ghost-scheduler');
const signedAdminToken = getSignedAdminToken({publishedAt: date.toISOString(), apiUrl, key});
const url = new URL(urlUtils.urlJoin(apiUrl, 'automations', 'poll'));
url.searchParams.set('token', signedAdminToken);
schedulerAdapter.schedule({time: date.getTime(), url: url.toString(), extra: {httpMethod: 'PUT'}});
} catch (err) {
logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
}
};

domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
enqueueAnotherPollAt: enqueuePollAt
})));

domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => welcomeEmailAutomationPoll({
memberWelcomeEmailService,
enqueueAnotherPollAt: enqueuePollAt
})));

schedulerAdapter.register(this);

enqueuePollAt(new Date());

this.#initialized = true;
}

/**
* Re-arm the poll chain. A queued poll signed under the previous scheduler
* key fails JWT verification when fired; this dispatches a fresh in-process
* poll that re-schedules the next callback under the current key.
*/
rescheduleAll() {
this.#enqueuePollNow?.();
}
}
const AutomationsService = require('./service');

module.exports = new AutomationsService();
89 changes: 89 additions & 0 deletions ghost/core/core/server/services/automations/service.js
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git makes this diff look huge! Here's what actually changed in this file (it's just one line!):

-module.exports = new AutomationsService();
+module.exports = AutomationsService;

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// @ts-check
const urlUtils = require('../../../shared/url-utils');
const {oneAtATime} = require('../../../shared/one-at-a-time');
const logging = require('@tryghost/logging');
const {getSignedAdminToken} = require('../../adapters/scheduling/utils');
const StartAutomationsPollEvent = require('./events/start-automations-poll-event');
const {poll} = require('./poll');
const {welcomeEmailAutomationPoll} = require('./welcome-email-automation-poll');
const memberWelcomeEmailService = require('../member-welcome-emails/service');
/** @import DomainEvents from '@tryghost/domain-events' */

/**
* @internal
* @typedef {object} SchedulerAdapter
* @prop {(job: {
* time: number;
* url: string;
* extra: {
* httpMethod: string;
* };
* }) => void} schedule
* @prop {(rescheduler: {rescheduleAll: () => unknown}) => void} register
*/

class AutomationsService {
#initialized = false;
#enqueuePollNow;

/**
* @param {object} options
* @param {Pick<DomainEvents, 'dispatch' | 'subscribe'>} options.domainEvents
* @param {string} options.apiUrl
* @param {SchedulerAdapter} options.schedulerAdapter
* @param {ReadonlyMap<string, Promise<{id: string, secret: string}>>} options.internalKeys
* @returns {void}
*/
init({domainEvents, apiUrl, schedulerAdapter, internalKeys}) {
if (this.#initialized) {
return;
}

this.#enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());

/** @param {Readonly<Date>} date */
const enqueuePollAt = async (date) => {
const isRequestedDateInTheFuture = new Date() < date;
if (!isRequestedDateInTheFuture) {
this.#enqueuePollNow();
return;
}

try {
const key = await internalKeys.get('ghost-scheduler');
const signedAdminToken = getSignedAdminToken({publishedAt: date.toISOString(), apiUrl, key});
const url = new URL(urlUtils.urlJoin(apiUrl, 'automations', 'poll'));
url.searchParams.set('token', signedAdminToken);
schedulerAdapter.schedule({time: date.getTime(), url: url.toString(), extra: {httpMethod: 'PUT'}});
} catch (err) {
logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
}
};

domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
enqueueAnotherPollAt: enqueuePollAt
})));

domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => welcomeEmailAutomationPoll({
memberWelcomeEmailService,
enqueueAnotherPollAt: enqueuePollAt
})));

schedulerAdapter.register(this);

enqueuePollAt(new Date());

this.#initialized = true;
}

/**
* Re-arm the poll chain. A queued poll signed under the previous scheduler
* key fails JWT verification when fired; this dispatches a fresh in-process
* poll that re-schedules the next callback under the current key.
*/
rescheduleAll() {
this.#enqueuePollNow?.();
}
}

module.exports = AutomationsService;
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
const sinon = require('sinon');

const StartAutomationsPollEvent = require('../../../../../core/server/services/automations/events/start-automations-poll-event');

const automationsModulePath = require.resolve('../../../../../core/server/services/automations');
const AutomationsService = require('../../../../../core/server/services/automations/service');

describe('automations service', function () {
let automations;
Expand All @@ -11,9 +10,7 @@ describe('automations service', function () {
let initOptions;

beforeEach(function () {
// Reset the module-level singleton between tests.
delete require.cache[automationsModulePath];
automations = require(automationsModulePath);
automations = new AutomationsService();
domainEvents = {
dispatch: sinon.stub(),
subscribe: sinon.stub()
Expand Down
Loading