Skip to content

Commit

Permalink
feat: Follow current trigger jobs even after the first load
Browse files Browse the repository at this point in the history
Now follow the the future update of the current trigger by following any
job related to it with realtime

Anytime a new job related to the current trigger is detected, the
ConnectionFlow will detect it, refetch the current trigger, to let the
stack determine the current job (if multiple job are running at the same
time), and follow the new current job

This will show the current trigger is running to the user and update the
result at the end of the job.
  • Loading branch information
doubleface authored and doubleface committed Nov 29, 2022
1 parent eda5b28 commit 563f270
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 24 deletions.
Expand Up @@ -15,11 +15,14 @@ const client = {
uri: 'https://cozy.tools:8080'
},
plugins: {
realtime: jest.fn()
realtime: { subscribe: jest.fn(), unsubscribe: jest.fn() }
}
}
const trigger = {
_id: 'trigger-id'
_id: 'trigger-id',
current_state: {
last_executed_job_id: 'testjobid'
}
}

const triggersMutationsMock = {
Expand Down
Expand Up @@ -5,6 +5,7 @@ import KonnectorAccountTabs, {
KonnectorAccountTabsTabs
} from './KonnectorAccountTabs'
import useMaintenanceStatus from 'components/hooks/useMaintenanceStatus'
import CozyClient from 'cozy-client/dist/CozyClient'

jest.mock('components/hooks/useMaintenanceStatus')
jest.mock(
Expand Down Expand Up @@ -84,8 +85,16 @@ describe('Konnector account tabs content', () => {
? { partnership: { domain: 'budget-insight.com' } }
: {}

const client = new CozyClient()
client.plugins = {
realtime: {
subscribe: jest.fn(),
unsubscribe: jest.fn()
}
}

const root = await render(
<AppLike>
<AppLike client={client}>
<KonnectorAccountTabs
konnector={konnector}
initialTrigger={trigger}
Expand Down
65 changes: 58 additions & 7 deletions packages/cozy-harvest-lib/src/components/KonnectorModal.spec.jsx
Expand Up @@ -37,7 +37,14 @@ describe('KonnectorModal', () => {
attributes: {},
triggers: {
data: [
{ _id: 784, doctype: 'io.cozy.triggers', arguments: '* * * * *' }
{
_id: 784,
doctype: 'io.cozy.triggers',
arguments: '* * * * *',
current_state: {
last_executed_job_id: 'jobid784'
}
}
]
}
}
Expand All @@ -62,6 +69,12 @@ describe('KonnectorModal', () => {
...extraProps
}
const client = new CozyClient()
client.plugins = {
realtime: {
subscribe: jest.fn(),
unsubscribe: jest.fn()
}
}
const root = render(
<AppLike client={client}>
<KonnectorModal {...finalProps} />
Expand Down Expand Up @@ -91,8 +104,22 @@ describe('KonnectorModal', () => {

it('should render the selected account via a prop', async () => {
mockKonnector.triggers.data = [
{ _id: '784', doctype: 'io.cozy.triggers', arguments: '* * * * *' },
{ _id: '872', doctype: 'io.cozy.triggers', arguments: '* * 1 1 1' }
{
_id: '784',
doctype: 'io.cozy.triggers',
arguments: '* * * * *',
current_state: {
last_executed_job_id: 'job_trigger_784'
}
},
{
_id: '872',
doctype: 'io.cozy.triggers',
arguments: '* * 1 1 1',
current_state: {
last_executed_job_id: 'job_trigger_872'
}
}
]
const { root } = await setup({
accountId: '123'
Expand All @@ -102,8 +129,20 @@ describe('KonnectorModal', () => {

it('should show the list of accounts', async () => {
mockKonnector.triggers.data = [
{ _id: '784', doctype: 'io.cozy.triggers' },
{ _id: '872', doctype: 'io.cozy.triggers' }
{
_id: '784',
doctype: 'io.cozy.triggers',
current_state: {
last_executed_job_id: 'job_trigger_784'
}
},
{
_id: '872',
doctype: 'io.cozy.triggers',
current_state: {
last_executed_job_id: 'job_trigger_872'
}
}
]
const { root } = setup()

Expand All @@ -112,8 +151,20 @@ describe('KonnectorModal', () => {

it('should request account creation', async () => {
mockKonnector.triggers.data = [
{ _id: '784', doctype: 'io.cozy.triggers' },
{ _id: '872', doctype: 'io.cozy.triggers' }
{
_id: '784',
doctype: 'io.cozy.triggers',
current_state: {
last_executed_job_id: 'job_trigger_784'
}
},
{
_id: '872',
doctype: 'io.cozy.triggers',
current_state: {
last_executed_job_id: 'job_trigger_872'
}
}
]
const createAction = jest.fn()
const { root } = setup({
Expand Down
Expand Up @@ -77,6 +77,10 @@ const mockVaultClient = {
const tMock = jest.fn()

const client = new CozyClient({})
client.plugins.realtime = {
subscribe: jest.fn(),
unsubscribe: jest.fn()
}
const props = {
konnector: fixtures.konnector,
flow: new ConnectionFlow(client, undefined, fixtures.konnector),
Expand Down
12 changes: 10 additions & 2 deletions packages/cozy-harvest-lib/src/components/TwoFAModal.spec.jsx
Expand Up @@ -12,11 +12,19 @@ describe('TwoFAModal', () => {
const client = {
on: jest.fn(),
plugins: {
realtime: jest.fn()
realtime: {
subscribe: jest.fn(),
unsubscribe: jest.fn()
}
}
}

const trigger = {}
const trigger = {
_id: 'triggerid',
current_state: {
last_executed_job_id: 'testjobid'
}
}
const konnector = {
slug: konnectorSlug
}
Expand Down
51 changes: 47 additions & 4 deletions packages/cozy-harvest-lib/src/models/ConnectionFlow.js
@@ -1,3 +1,4 @@
// @ts-check
import MicroEE from 'microee'

import flag from 'cozy-flags'
Expand Down Expand Up @@ -158,7 +159,8 @@ export class ConnectionFlow {
this.getTwoFACodeProvider = this.getTwoFACodeProvider.bind(this)
this.getKonnectorSlug = this.getKonnectorSlug.bind(this)
this.handleAccountUpdated = this.handleAccountUpdated.bind(this)
this.handleJobUpdated = this.handleJobUpdated.bind(this)
this.handleCurrentJobUpdated = this.handleCurrentJobUpdated.bind(this)
this.handleTriggerJobUpdated = this.handleTriggerJobUpdated.bind(this)
this.handleAccountTwoFA = this.handleAccountTwoFA.bind(this)
this.launch = this.launch.bind(this)
this.sendTwoFACode = this.sendTwoFACode.bind(this)
Expand All @@ -177,6 +179,7 @@ export class ConnectionFlow {
this.realtime = client.plugins.realtime

this.watchCurrentJobIfTriggerIsAlreadyRunning()
this.watchTriggerJobs()
}

getTwoFACodeProvider() {
Expand Down Expand Up @@ -295,6 +298,13 @@ export class ConnectionFlow {
})
}

/**
* Set a state to the flow to show that the current trigger is running while the job is not
* created yet (the job will be created by a webhook)
*
* @param {Object} options
* @param {import('cozy-client/types/types').KonnectorsDoctype} options.konnector
*/
expectTriggerLaunch({ konnector }) {
this.konnector = konnector
logger.info(
Expand Down Expand Up @@ -483,9 +493,16 @@ export class ConnectionFlow {
}
}

async handleJobUpdated() {
handleCurrentJobUpdated() {
logger.debug('ConnectionFlow: Handling update from job')
await this.refetchTrigger()
this.refetchTrigger()
}

handleTriggerJobUpdated(job) {
if (job.trigger_id !== this.trigger?._id) return // filter out jobs associated to other triggers
if (job._id === this.job?._id) return // if the event is associated to a job we are already watchin, ignore it. No need to refetch the current trigger in this case

this.refetchTrigger()
}

async refetchTrigger() {
Expand All @@ -496,6 +513,9 @@ export class ConnectionFlow {
const trigger = await fetchTrigger(this.client, this.trigger._id)
logger.debug(`Refetched trigger`, trigger)
this.trigger = trigger

this.watchCurrentJobIfTriggerIsAlreadyRunning()

this.emit(UPDATE_EVENT)
}

Expand Down Expand Up @@ -624,12 +644,35 @@ export class ConnectionFlow {
}
}

watchTriggerJobs() {
// can happen when the current trigger comes from realtime
if (this.trigger) {
// When the trigger comes from realtime, cozy-stack does not add the current_state to the object. So we need to request the stack to get it
if (!this.trigger?.current_state) {
this.refetchTrigger()
}
this.realtime.subscribe(
'updated',
JOBS_DOCTYPE,
this.handleTriggerJobUpdated
)
}
}

watchJob(options = { autoSuccessTimer: false }) {
if (this.job?._id === this.jobWatcher?.job?._id) {
// no need to rewatch a job we are already watching
return
}
if (this.jobWatcher) {
// if no job has been watched yet, there is not jobWatcher yet.
this.jobWatcher.unsubscribeAll()
}
this.realtime.subscribe(
'updated',
JOBS_DOCTYPE,
this.job._id,
this.handleJobUpdated.bind(this)
this.handleCurrentJobUpdated.bind(this)
)
this.jobWatcher = watchKonnectorJob(this.client, this.job, options)
logger.info(`ConnectionFlow: Subscribed to ${JOBS_DOCTYPE}:${this.job._id}`)
Expand Down
44 changes: 37 additions & 7 deletions packages/cozy-harvest-lib/src/models/ConnectionFlow.spec.js
Expand Up @@ -5,6 +5,7 @@ import { saveAccount } from '../connections/accounts'
import {
createTrigger,
ensureTrigger,
fetchTrigger,
prepareTriggerAccount,
launchTrigger
} from '../connections/triggers'
Expand Down Expand Up @@ -57,10 +58,11 @@ const realtimeMock = {
events: new EventEmitter(),
key: (action, doctype) => `${doctype}:${action}`,
subscribtions: new Map(),
subscribe: jest.fn().mockImplementation((action, doctype, callback) => {
subscribe: jest.fn().mockImplementation((action, doctype, id, callback) => {
const finalcallback = callback === undefined ? id : callback
const { events, key, subscribtions } = realtimeMock
const subscribtionKey = key(action, doctype)
subscribtions.set(subscribtionKey, data => callback(data))
subscribtions.set(subscribtionKey, data => finalcallback(data))
events.on(
subscribtionKey,
realtimeMock.subscribtions.get(key(action, doctype))
Expand Down Expand Up @@ -88,7 +90,8 @@ jest.mock('../connections/triggers', () => {
createTrigger: jest.fn(),
ensureTrigger: jest.fn(),
prepareTriggerAccount: jest.fn(),
launchTrigger: jest.fn()
launchTrigger: jest.fn(),
fetchTrigger: jest.fn()
}
})

Expand Down Expand Up @@ -164,10 +167,40 @@ describe('ConnectionFlow', () => {
setup({ trigger: fixtures.runningTrigger })
expect(watchKonnectorJob).toHaveBeenCalledWith(
expect.any(Object),
{ _id: 'runningjobid' },
{ _id: 'running-job-id' },
{ autoSuccessTimer: false }
)
})

it('should watch all jobs related to the current trigger', async () => {
setup({ trigger: fixtures.createdTrigger })
expect(fetchTrigger).not.toHaveBeenCalled()
realtimeMock.events.emit(realtimeMock.key('updated', 'io.cozy.jobs'), {
...fixtures.runningJob,
trigger_id: 'created-trigger-id'
})
await new Promise(process.nextTick) // await all promises to be resolved
expect(fetchTrigger).toHaveBeenLastCalledWith(
expect.anything(),
'created-trigger-id'
)
})

it('should watch watch future jobs for a trigger if new fetched trigger is running', async () => {
const { flow } = setup({ trigger: fixtures.createdTrigger })
expect(fetchTrigger).not.toHaveBeenCalled()
fetchTrigger.mockResolvedValueOnce(fixtures.runningTrigger)
expect(flow.job).toBeUndefined()
realtimeMock.events.emit(realtimeMock.key('updated', 'io.cozy.jobs'), {
...fixtures.runningJob,
trigger_id: 'created-trigger-id'
})
await new Promise(process.nextTick) // await all promises to be resolved
expect(fetchTrigger).toHaveBeenCalledTimes(1)
expect(flow.job).toStrictEqual({
_id: 'running-job-id'
})
})
})

describe('getState', () => {
Expand Down Expand Up @@ -675,6 +708,3 @@ describe('ConnectionFlow', () => {
})
})
})

// it should have running false on trigger updates
// it should set error on trigger updates
Expand Up @@ -14,6 +14,9 @@ describe('watchKonnectorJob', () => {
const client = new CozyClient({
uri: 'http://cozy.tools:8080'
})
client.plugins = {
realtime: { subscribe: jest.fn(), unsubscribe: jest.fn() }
}
client.on = jest.fn()
const result = await watchKonnectorJob(client, job)
expect(result instanceof KonnectorJobWatcher).toBe(true)
Expand Down
7 changes: 7 additions & 0 deletions packages/cozy-harvest-lib/src/services/budget-insight.spec.js
Expand Up @@ -89,11 +89,17 @@ const account = {

const sleep = duration => new Promise(resolve => setTimeout(resolve, duration))

const realtimeMock = {
subscribe: jest.fn(),
unsubscribe: jest.fn()
}

describe('finishConnection', () => {
const setup = () => {
const client = new CozyClient({
uri: 'http://testcozy.mycozy.cloud'
})
client.plugins = { realtime: realtimeMock }
const flow = new ConnectionFlow(client, { konnector, account })
return { flow }
}
Expand Down Expand Up @@ -276,6 +282,7 @@ describe('createOrUpdateBIConnection', () => {
const client = new CozyClient({
uri: 'http://testcozy.mycozy.cloud'
})
client.plugins = { realtime: realtimeMock }
const flow = new ConnectionFlow(client, { konnector, account })
client.stackClient.jobs.create = jest.fn().mockReturnValue({
data: {
Expand Down

0 comments on commit 563f270

Please sign in to comment.