fix(plugin-workflow): fix schedule trigger bug (#949)
This commit is contained in:
		
							parent
							
								
									a47ca1dd6c
								
							
						
					
					
						commit
						9b8a4d1063
					
				@ -40,7 +40,7 @@ export default observer(({ value, onChange }: any) => {
 | 
				
			|||||||
      // && (!['linkTo', 'hasMany', 'hasOne', 'belongsToMany'].includes(field.type))
 | 
					      // && (!['linkTo', 'hasMany', 'hasOne', 'belongsToMany'].includes(field.type))
 | 
				
			||||||
    ));
 | 
					    ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const fieldsSet = new Set(fields.map(field => field.name));
 | 
					  const unassignedFields = fields.filter(field => !(field.name in value));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <fieldset className={css`
 | 
					    <fieldset className={css`
 | 
				
			||||||
@ -131,16 +131,14 @@ export default observer(({ value, onChange }: any) => {
 | 
				
			|||||||
                );
 | 
					                );
 | 
				
			||||||
              })
 | 
					              })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            {Object.keys(value).filter(key => fieldsSet.has(key)).length < fields.length
 | 
					            {unassignedFields.length
 | 
				
			||||||
              ? (
 | 
					              ? (
 | 
				
			||||||
                <Dropdown overlay={
 | 
					                <Dropdown overlay={
 | 
				
			||||||
                  <Menu onClick={({ key }) => onChange({ ...value, [key]: null })} className={css`
 | 
					                  <Menu onClick={({ key }) => onChange({ ...value, [key]: null })} className={css`
 | 
				
			||||||
                    max-height: 300px;
 | 
					                    max-height: 300px;
 | 
				
			||||||
                    overflow-y: auto;
 | 
					                    overflow-y: auto;
 | 
				
			||||||
                  `}>
 | 
					                  `}>
 | 
				
			||||||
                    {fields
 | 
					                    {unassignedFields.map(field => (
 | 
				
			||||||
                      .filter(field => !(field.name in value))
 | 
					 | 
				
			||||||
                      .map(field => (
 | 
					 | 
				
			||||||
                      <Menu.Item key={field.name}>{compile(field.uiSchema?.title ?? field.name)}</Menu.Item>
 | 
					                      <Menu.Item key={field.name}>{compile(field.uiSchema?.title ?? field.name)}</Menu.Item>
 | 
				
			||||||
                    ))}
 | 
					                    ))}
 | 
				
			||||||
                  </Menu>
 | 
					                  </Menu>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import { getApp, sleep } from '..';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe.skip('workflow > triggers > schedule', () => {
 | 
					describe('workflow > triggers > schedule', () => {
 | 
				
			||||||
  let app: Application;
 | 
					  let app: Application;
 | 
				
			||||||
  let db: Database;
 | 
					  let db: Database;
 | 
				
			||||||
  let PostRepo;
 | 
					  let PostRepo;
 | 
				
			||||||
@ -24,7 +24,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
  afterEach(() => app.destroy());
 | 
					  afterEach(() => app.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('constant mode', () => {
 | 
					  describe('constant mode', () => {
 | 
				
			||||||
    it('no repeat configurated', async () => {
 | 
					    it('neither startsOn nor repeat configurated', async () => {
 | 
				
			||||||
      const workflow = await WorkflowModel.create({
 | 
					      const workflow = await WorkflowModel.create({
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        type: 'schedule',
 | 
					        type: 'schedule',
 | 
				
			||||||
@ -49,6 +49,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
        type: 'schedule',
 | 
					        type: 'schedule',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          mode: 0,
 | 
					          mode: 0,
 | 
				
			||||||
 | 
					          startsOn: now.toISOString(),
 | 
				
			||||||
          repeat: '*/2 * * * * *',
 | 
					          repeat: '*/2 * * * * *',
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -71,6 +72,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
        type: 'schedule',
 | 
					        type: 'schedule',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          mode: 0,
 | 
					          mode: 0,
 | 
				
			||||||
 | 
					          startsOn: now.toISOString(),
 | 
				
			||||||
          repeat: '*/2 * * * * *',
 | 
					          repeat: '*/2 * * * * *',
 | 
				
			||||||
          limit: 1
 | 
					          limit: 1
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -92,6 +94,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
        type: 'schedule',
 | 
					        type: 'schedule',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          mode: 0,
 | 
					          mode: 0,
 | 
				
			||||||
 | 
					          startsOn: now.toISOString(),
 | 
				
			||||||
          repeat: 2000,
 | 
					          repeat: 2000,
 | 
				
			||||||
          limit: 1
 | 
					          limit: 1
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -105,6 +108,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('on certain second', async () => {
 | 
					    it('on certain second', async () => {
 | 
				
			||||||
      const now = new Date();
 | 
					      const now = new Date();
 | 
				
			||||||
 | 
					      const startsOn = now.toISOString();
 | 
				
			||||||
      now.setSeconds(now.getSeconds() + 3);
 | 
					      now.setSeconds(now.getSeconds() + 3);
 | 
				
			||||||
      now.setMilliseconds(0);
 | 
					      now.setMilliseconds(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -113,6 +117,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
        type: 'schedule',
 | 
					        type: 'schedule',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          mode: 0,
 | 
					          mode: 0,
 | 
				
			||||||
 | 
					          startsOn,
 | 
				
			||||||
          repeat: `${now.getSeconds()} * * * * *`,
 | 
					          repeat: `${now.getSeconds()} * * * * *`,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -126,6 +131,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('multiple workflows trigger at same time', async () => {
 | 
					    it('multiple workflows trigger at same time', async () => {
 | 
				
			||||||
      const now = new Date();
 | 
					      const now = new Date();
 | 
				
			||||||
 | 
					      const startsOn = now.toISOString();
 | 
				
			||||||
      now.setSeconds(now.getSeconds() + 2);
 | 
					      now.setSeconds(now.getSeconds() + 2);
 | 
				
			||||||
      now.setMilliseconds(0);
 | 
					      now.setMilliseconds(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -137,6 +143,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
            type: 'schedule',
 | 
					            type: 'schedule',
 | 
				
			||||||
            config: {
 | 
					            config: {
 | 
				
			||||||
              mode: 0,
 | 
					              mode: 0,
 | 
				
			||||||
 | 
					              startsOn,
 | 
				
			||||||
              repeat: `${now.getSeconds()} * * * * *`,
 | 
					              repeat: `${now.getSeconds()} * * * * *`,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -151,6 +158,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
            type: 'schedule',
 | 
					            type: 'schedule',
 | 
				
			||||||
            config: {
 | 
					            config: {
 | 
				
			||||||
              mode: 0,
 | 
					              mode: 0,
 | 
				
			||||||
 | 
					              startsOn,
 | 
				
			||||||
              repeat: `${now.getSeconds()} * * * * *`,
 | 
					              repeat: `${now.getSeconds()} * * * * *`,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -205,7 +213,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
      expect(execution.context.date).toBe(triggerTime.toISOString());
 | 
					      expect(execution.context.date).toBe(triggerTime.toISOString());
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('starts on post.createdAt and repeat', async () => {
 | 
					    it('starts on post.createdAt and repeat by cron', async () => {
 | 
				
			||||||
      const workflow = await WorkflowModel.create({
 | 
					      const workflow = await WorkflowModel.create({
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        type: 'schedule',
 | 
					        type: 'schedule',
 | 
				
			||||||
@ -223,6 +231,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
      await sleep((2.5 - now.getSeconds() % 2) * 1000 - now.getMilliseconds());
 | 
					      await sleep((2.5 - now.getSeconds() % 2) * 1000 - now.getMilliseconds());
 | 
				
			||||||
      const startTime = new Date();
 | 
					      const startTime = new Date();
 | 
				
			||||||
      startTime.setMilliseconds(500);
 | 
					      startTime.setMilliseconds(500);
 | 
				
			||||||
 | 
					      console.log(startTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const post = await PostRepo.create({ values: { title: 't1' }});
 | 
					      const post = await PostRepo.create({ values: { title: 't1' }});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -230,7 +239,7 @@ describe.skip('workflow > triggers > schedule', () => {
 | 
				
			|||||||
      // sleep 1.5s at 2s trigger 1st time
 | 
					      // sleep 1.5s at 2s trigger 1st time
 | 
				
			||||||
      // sleep 3.5s at 4s trigger 2nd time
 | 
					      // sleep 3.5s at 4s trigger 2nd time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const executions = await workflow.getExecutions();
 | 
					      const executions = await workflow.getExecutions({ order: [['createdAt', 'ASC']] });
 | 
				
			||||||
      expect(executions.length).toBe(2);
 | 
					      expect(executions.length).toBe(2);
 | 
				
			||||||
      const d1 = Date.parse(executions[0].context.date);
 | 
					      const d1 = Date.parse(executions[0].context.date);
 | 
				
			||||||
      expect(d1 - 1500).toBe(startTime.getTime());
 | 
					      expect(d1 - 1500).toBe(startTime.getTime());
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import parser from 'cron-parser';
 | 
					import parser from 'cron-parser';
 | 
				
			||||||
import { literal, Op, where, fn } from 'sequelize';
 | 
					import { literal, Op, where, fn } from 'sequelize';
 | 
				
			||||||
import Plugin, { Trigger } from '..';
 | 
					import Plugin, { Trigger } from '..';
 | 
				
			||||||
 | 
					import WorkflowModel from '../models/Workflow';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ScheduleOnField = string | {
 | 
					export type ScheduleOnField = string | {
 | 
				
			||||||
  field: string;
 | 
					  field: string;
 | 
				
			||||||
@ -26,10 +27,10 @@ export const SCHEDULE_MODE = {
 | 
				
			|||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ScheduleMode {
 | 
					interface ScheduleMode {
 | 
				
			||||||
  on?(this: ScheduleTrigger, workflow): void;
 | 
					  on?(this: ScheduleTrigger, workflow: WorkflowModel): void;
 | 
				
			||||||
  off?(this: ScheduleTrigger, workflow): void;
 | 
					  off?(this: ScheduleTrigger, workflow: WorkflowModel): void;
 | 
				
			||||||
  shouldCache(this: ScheduleTrigger, workflow, now: Date): Promise<boolean> | boolean;
 | 
					  shouldCache(this: ScheduleTrigger, workflow: WorkflowModel, now: Date): Promise<boolean> | boolean;
 | 
				
			||||||
  trigger(this: ScheduleTrigger, workflow, now: Date): any;
 | 
					  trigger(this: ScheduleTrigger, workflow: WorkflowModel, now: Date): any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ScheduleModes = new Map<number, ScheduleMode>();
 | 
					const ScheduleModes = new Map<number, ScheduleMode>();
 | 
				
			||||||
@ -38,62 +39,77 @@ ScheduleModes.set(SCHEDULE_MODE.CONSTANT, {
 | 
				
			|||||||
  shouldCache(workflow, now) {
 | 
					  shouldCache(workflow, now) {
 | 
				
			||||||
    const { startsOn, endsOn, repeat } = workflow.config;
 | 
					    const { startsOn, endsOn, repeat } = workflow.config;
 | 
				
			||||||
    const timestamp = now.getTime();
 | 
					    const timestamp = now.getTime();
 | 
				
			||||||
    if (startsOn) {
 | 
					
 | 
				
			||||||
    const startTime = Date.parse(startsOn);
 | 
					    const startTime = Date.parse(startsOn);
 | 
				
			||||||
    if (!startTime || (startTime > timestamp + this.cacheCycle)) {
 | 
					    if (!startTime || (startTime > timestamp + this.cacheCycle)) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (repeat) {
 | 
				
			||||||
      if (typeof repeat === 'number'
 | 
					      if (typeof repeat === 'number'
 | 
				
			||||||
        && repeat > this.cacheCycle
 | 
					        && repeat > this.cacheCycle
 | 
				
			||||||
        && (timestamp - startTime) % repeat > this.cacheCycle
 | 
					        && (timestamp - startTime) % repeat > this.cacheCycle
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
      if (endsOn) {
 | 
					      if (endsOn) {
 | 
				
			||||||
        const endTime = Date.parse(endsOn);
 | 
					        const endTime = Date.parse(endsOn);
 | 
				
			||||||
        if (!endTime || endTime <= timestamp) {
 | 
					        if (!endTime || endTime <= timestamp) {
 | 
				
			||||||
          return false;
 | 
					          return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (startTime < timestamp) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  trigger(workflow, date) {
 | 
					  trigger(workflow, now) {
 | 
				
			||||||
    const { startsOn, endsOn, repeat } = workflow.config;
 | 
					    const { startsOn, endsOn, repeat } = workflow.config;
 | 
				
			||||||
    if (startsOn) {
 | 
					    const timestamp = now.getTime();
 | 
				
			||||||
    const startTime = Math.floor(Date.parse(startsOn) / 1000) * 1000;
 | 
					    const startTime = Math.floor(Date.parse(startsOn) / 1000) * 1000;
 | 
				
			||||||
 | 
					    if (repeat) {
 | 
				
			||||||
      if (typeof repeat === 'number') {
 | 
					      if (typeof repeat === 'number') {
 | 
				
			||||||
        if (Math.round(date.getTime() - startTime) % repeat) {
 | 
					        if (Math.round(timestamp - startTime) % repeat) {
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (endsOn) {
 | 
				
			||||||
 | 
					        const endTime = Date.parse(endsOn);
 | 
				
			||||||
 | 
					        if (!endTime || endTime < timestamp) {
 | 
				
			||||||
 | 
					          return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    return this.plugin.trigger(workflow, { date });
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (startTime !== timestamp) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.plugin.trigger(workflow, { date: now });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getDateRangeFilter(on: ScheduleOnField, now: Date, dir: number) {
 | 
					function getOnTimestampWithOffset(on, now: Date) {
 | 
				
			||||||
  const timestamp = now.getTime();
 | 
					 | 
				
			||||||
  const op = dir < 0 ? Op.lt : Op.gte;
 | 
					 | 
				
			||||||
  switch (typeof on) {
 | 
					  switch (typeof on) {
 | 
				
			||||||
    case 'string':
 | 
					    case 'string':
 | 
				
			||||||
      const time = Date.parse(on);
 | 
					      return Date.parse(on);
 | 
				
			||||||
      if (!time || (dir < 0 ? (timestamp < time) : (time <= timestamp))) {
 | 
					 | 
				
			||||||
        return null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    case 'object':
 | 
					    case 'object':
 | 
				
			||||||
      const { field, offset = 0, unit = 1000 } = on;
 | 
					      const { field, offset = 0, unit = 1000 } = on;
 | 
				
			||||||
      if (!field) {
 | 
					      if (!field) {
 | 
				
			||||||
        return {};
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return { [field]: { [op]: new Date(timestamp + offset * unit * dir) } };
 | 
					      const timestamp = now.getTime();
 | 
				
			||||||
 | 
					      // onDate + offset > now
 | 
				
			||||||
 | 
					      // onDate > now - offset
 | 
				
			||||||
 | 
					      return timestamp - offset * unit;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      break;
 | 
					      return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {};
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getDataOptionTime(data, on, dir = 1) {
 | 
					function getDataOptionTime(data, on, dir = 1) {
 | 
				
			||||||
@ -103,13 +119,13 @@ function getDataOptionTime(data, on, dir = 1) {
 | 
				
			|||||||
      return time ? time : null;
 | 
					      return time ? time : null;
 | 
				
			||||||
    case 'object':
 | 
					    case 'object':
 | 
				
			||||||
      const { field, offset = 0, unit = 1000 } = on;
 | 
					      const { field, offset = 0, unit = 1000 } = on;
 | 
				
			||||||
      return data[field] ? data[field].getTime() - offset * unit * dir : null;
 | 
					      return data.get(field) ? data.get(field).getTime() - offset * unit * dir : null;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getHookId(workflow, type) {
 | 
					function getHookId(workflow, type: string) {
 | 
				
			||||||
  return `${type}#${workflow.id}`;
 | 
					  return `${type}#${workflow.id}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,7 +147,9 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
    const { collection, startsOn, endsOn, repeat } = workflow.config;
 | 
					    const { collection, startsOn, endsOn, repeat } = workflow.config;
 | 
				
			||||||
    const event = `${collection}.afterSave`;
 | 
					    const event = `${collection}.afterSave`;
 | 
				
			||||||
    const name = getHookId(workflow, event);
 | 
					    const name = getHookId(workflow, event);
 | 
				
			||||||
    if (!this.events.has(name)) {
 | 
					    if (this.events.has(name)) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    // NOTE: toggle cache depends on new date
 | 
					    // NOTE: toggle cache depends on new date
 | 
				
			||||||
    const listener = async (data, options) => {
 | 
					    const listener = async (data, options) => {
 | 
				
			||||||
      const now = new Date();
 | 
					      const now = new Date();
 | 
				
			||||||
@ -148,7 +166,7 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
      if (endTime && endTime <= timestamp) {
 | 
					      if (endTime && endTime <= timestamp) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
        if (!nextInCycle.call(this, workflow, now)) {
 | 
					      if (!matchNext.call(this, workflow, now)) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (typeof repeat === 'number'
 | 
					      if (typeof repeat === 'number'
 | 
				
			||||||
@ -157,13 +175,11 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
        console.log('set cache', now);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.setCache(workflow);
 | 
					      this.setCache(workflow);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    this.events.set(name, listener);
 | 
					    this.events.set(name, listener);
 | 
				
			||||||
      this.plugin.app.db.on(`${collection}.afterSave`, listener);
 | 
					    this.plugin.app.db.on(event, listener);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  off(workflow) {
 | 
					  off(workflow) {
 | 
				
			||||||
@ -173,37 +189,66 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
    if (this.events.has(name)) {
 | 
					    if (this.events.has(name)) {
 | 
				
			||||||
      const listener = this.events.get(name);
 | 
					      const listener = this.events.get(name);
 | 
				
			||||||
      this.events.delete(name);
 | 
					      this.events.delete(name);
 | 
				
			||||||
      this.plugin.app.db.off(`${collection}.afterSave`, listener);
 | 
					      this.plugin.app.db.off(event, listener);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async shouldCache(workflow, now) {
 | 
					  async shouldCache(workflow, now) {
 | 
				
			||||||
 | 
					    const { db } = this.plugin.app;
 | 
				
			||||||
    const { startsOn, endsOn, repeat, collection } = workflow.config;
 | 
					    const { startsOn, endsOn, repeat, collection } = workflow.config;
 | 
				
			||||||
    const starts = getDateRangeFilter(startsOn, now, -1);
 | 
					    const timestamp = now.getTime();
 | 
				
			||||||
    if (!starts || !Object.keys(starts).length) {
 | 
					
 | 
				
			||||||
      return false;
 | 
					    const startTimestamp = getOnTimestampWithOffset(startsOn, now);
 | 
				
			||||||
    }
 | 
					    if (!startTimestamp) {
 | 
				
			||||||
    const ends = getDateRangeFilter(endsOn, now, 1);
 | 
					 | 
				
			||||||
    if (!ends) {
 | 
					 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const conditions: any[] = [starts, ends].filter(item => Boolean(Object.keys(item).length));
 | 
					    const conditions: any[] = [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        [startsOn.field]: {
 | 
				
			||||||
 | 
					          [Op.lt]: new Date(startTimestamp + this.cacheCycle)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // when repeat is number, means repeat after startsOn
 | 
					    // when repeat is number, means repeat after startsOn
 | 
				
			||||||
    // (now - startsOn) % repeat <= cacheCycle
 | 
					    // (now - startsOn) % repeat <= cacheCycle
 | 
				
			||||||
    const { db } = this.plugin.app;
 | 
					    if (repeat) {
 | 
				
			||||||
      const tsFn = DialectTimestampFnMap[db.options.dialect];
 | 
					      const tsFn = DialectTimestampFnMap[db.options.dialect];
 | 
				
			||||||
    if (repeat
 | 
					      if (typeof repeat === 'number'
 | 
				
			||||||
      && typeof repeat === 'number'
 | 
					 | 
				
			||||||
        && repeat > this.cacheCycle
 | 
					        && repeat > this.cacheCycle
 | 
				
			||||||
        && tsFn
 | 
					        && tsFn
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
      const uts = now.getTime();
 | 
					 | 
				
			||||||
        conditions.push(where(
 | 
					        conditions.push(where(
 | 
				
			||||||
        fn('MOD', literal(`${Math.round(uts / 1000)} - ${tsFn(startsOn.field)}`), Math.round(repeat / 1000)),
 | 
					          fn('MOD', literal(`${Math.round(timestamp / 1000)} - ${tsFn(startsOn.field)}`), Math.round(repeat / 1000)),
 | 
				
			||||||
          { [Op.lt]: Math.round(this.cacheCycle / 1000) }
 | 
					          { [Op.lt]: Math.round(this.cacheCycle / 1000) }
 | 
				
			||||||
        ));
 | 
					        ));
 | 
				
			||||||
      // conditions.push(literal(`mod(${uts} - ${tsFn(startsOn.field)} * 1000, ${repeat}) < ${this.cacheCycle}`));
 | 
					        // conditions.push(literal(`mod(${timestamp} - ${tsFn(startsOn.field)} * 1000, ${repeat}) < ${this.cacheCycle}`));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (endsOn) {
 | 
				
			||||||
 | 
					        const endTimestamp = getOnTimestampWithOffset(endsOn, now);
 | 
				
			||||||
 | 
					        if (!endTimestamp) {
 | 
				
			||||||
 | 
					          return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (typeof endsOn === 'string') {
 | 
				
			||||||
 | 
					          if (endTimestamp <= timestamp) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          conditions.push({
 | 
				
			||||||
 | 
					            [endsOn.field]: {
 | 
				
			||||||
 | 
					              [Op.gte]: new Date(endTimestamp + this.interval)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      conditions.push({
 | 
				
			||||||
 | 
					        [startsOn.field]: {
 | 
				
			||||||
 | 
					          [Op.gte]: new Date(startTimestamp)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { model } = db.getCollection(collection);
 | 
					    const { model } = db.getCollection(collection);
 | 
				
			||||||
@ -214,31 +259,24 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
    return Boolean(count);
 | 
					    return Boolean(count);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async trigger(workflow, date) {
 | 
					  async trigger(workflow, now: Date) {
 | 
				
			||||||
    const {
 | 
					    const { startsOn, repeat, endsOn, collection } = workflow.config;
 | 
				
			||||||
      collection,
 | 
					    const timestamp = now.getTime();
 | 
				
			||||||
      startsOn,
 | 
					 | 
				
			||||||
      endsOn,
 | 
					 | 
				
			||||||
      repeat
 | 
					 | 
				
			||||||
    } = workflow.config;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (typeof startsOn !== 'object') {
 | 
					    const startTimestamp = getOnTimestampWithOffset(startsOn, now);
 | 
				
			||||||
      return;
 | 
					    if (!startTimestamp) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timestamp = date.getTime();
 | 
					    const conditions: any[] = [
 | 
				
			||||||
    const startTimestamp = timestamp - (startsOn.offset ?? 0) * (startsOn.unit ?? 1000);
 | 
					      {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const conditions = [];
 | 
					 | 
				
			||||||
    if (!repeat) {
 | 
					 | 
				
			||||||
      // startsOn exactly equal to now in 1s
 | 
					 | 
				
			||||||
      conditions.push({
 | 
					 | 
				
			||||||
        [startsOn.field]: {
 | 
					        [startsOn.field]: {
 | 
				
			||||||
          [Op.gte]: new Date(startTimestamp),
 | 
					          [Op.lt]: new Date(startTimestamp + this.interval)
 | 
				
			||||||
          [Op.lt]: new Date(startTimestamp + 1000)
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (repeat) {
 | 
				
			||||||
      // startsOn not after now
 | 
					      // startsOn not after now
 | 
				
			||||||
      conditions.push({
 | 
					      conditions.push({
 | 
				
			||||||
        [startsOn.field]: {
 | 
					        [startsOn.field]: {
 | 
				
			||||||
@ -255,25 +293,31 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
        // conditions.push(literal(`MOD(CAST(${timestamp} AS BIGINT) - CAST((FLOOR(${tsFn(startsOn.field)}) AS BIGINT) * 1000), ${repeat}) = 0`));
 | 
					        // conditions.push(literal(`MOD(CAST(${timestamp} AS BIGINT) - CAST((FLOOR(${tsFn(startsOn.field)}) AS BIGINT) * 1000), ${repeat}) = 0`));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      switch (typeof endsOn) {
 | 
					      if (endsOn) {
 | 
				
			||||||
        case 'string':
 | 
					        const endTimestamp = getOnTimestampWithOffset(endsOn, now);
 | 
				
			||||||
          const endTime = Date.parse(endsOn);
 | 
					        if (!endTimestamp) {
 | 
				
			||||||
          if (!endTime || endTime <= timestamp) {
 | 
					          return false;
 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
          break;
 | 
					
 | 
				
			||||||
        case 'object':
 | 
					        if (typeof endsOn === 'string') {
 | 
				
			||||||
          if (endsOn.field) {
 | 
					          if (endTimestamp <= timestamp) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
          conditions.push({
 | 
					          conditions.push({
 | 
				
			||||||
            [endsOn.field]: {
 | 
					            [endsOn.field]: {
 | 
				
			||||||
                [Op.gte]: new Date(timestamp - (endsOn.offset ?? 0) * (endsOn.unit ?? 1000) + 1000)
 | 
					              [Op.gte]: new Date(endTimestamp + this.interval)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
          break;
 | 
					 | 
				
			||||||
        default:
 | 
					 | 
				
			||||||
          break;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // startsOn exactly equal to now in 1s
 | 
				
			||||||
 | 
					      conditions.push({
 | 
				
			||||||
 | 
					        [startsOn.field]: {
 | 
				
			||||||
 | 
					          [Op.gte]: new Date(startTimestamp)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { model } = this.plugin.app.db.getCollection(collection);
 | 
					    const { model } = this.plugin.app.db.getCollection(collection);
 | 
				
			||||||
@ -284,12 +328,12 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (instances.length) {
 | 
					    if (instances.length) {
 | 
				
			||||||
      console.log(instances.length, 'rows trigger at', date);
 | 
					      console.log(instances.length, 'rows trigger at', now);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    instances.forEach(item => {
 | 
					    instances.forEach(item => {
 | 
				
			||||||
      this.plugin.trigger(workflow, {
 | 
					      this.plugin.trigger(workflow, {
 | 
				
			||||||
        date,
 | 
					        date: now,
 | 
				
			||||||
        data: item.get()
 | 
					        data: item.get()
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -297,55 +341,12 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function nextInCycle(this: ScheduleTrigger, workflow, now: Date): boolean {
 | 
					function matchNext(this: ScheduleTrigger, workflow, now: Date, range: number = this.cacheCycle): boolean {
 | 
				
			||||||
  const { repeat } = workflow.config;
 | 
					  const { repeat } = workflow.config;
 | 
				
			||||||
  // no repeat means no need to rerun
 | 
					  // no repeat means no need to rerun
 | 
				
			||||||
  // but if in current cycle, should be put in cache
 | 
					  // but if in current cycle, should be put in cache
 | 
				
			||||||
  // no repeat but in current cycle means startsOn has been configured
 | 
					  // no repeat but in current cycle means startsOn has been configured
 | 
				
			||||||
  // so we need to more info to determine if necessary config items
 | 
					  // so we need to more info to determine if necessary config items
 | 
				
			||||||
  if (!repeat) {
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  switch (typeof repeat) {
 | 
					 | 
				
			||||||
    case 'string':
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    default:
 | 
					 | 
				
			||||||
      return true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const currentDate = new Date(now);
 | 
					 | 
				
			||||||
  currentDate.setMilliseconds(-1);
 | 
					 | 
				
			||||||
  const timestamp = now.getTime();
 | 
					 | 
				
			||||||
  const interval = parser.parseExpression(repeat, { currentDate });
 | 
					 | 
				
			||||||
  let next = interval.next();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // NOTE: cache all workflows will be matched in current cycle
 | 
					 | 
				
			||||||
  if (next.getTime() - timestamp <= this.cacheCycle) {
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export default class ScheduleTrigger extends Trigger {
 | 
					 | 
				
			||||||
  static CacheRules = [
 | 
					 | 
				
			||||||
    ({ config, allExecuted }) => config.limit ? allExecuted < config.limit : true,
 | 
					 | 
				
			||||||
    ({ config, executed }) => !executed || config.repeat != null,
 | 
					 | 
				
			||||||
    ({ config }) => ['repeat', 'startsOn'].some(key => config[key]),
 | 
					 | 
				
			||||||
    nextInCycle,
 | 
					 | 
				
			||||||
    function(workflow, now) {
 | 
					 | 
				
			||||||
      const { mode } = workflow.config;
 | 
					 | 
				
			||||||
      const modeHandlers = ScheduleModes.get(mode);
 | 
					 | 
				
			||||||
      return modeHandlers.shouldCache.call(this, workflow, now);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static TriggerRules = [
 | 
					 | 
				
			||||||
    ({ config, allExecuted }) => config.limit ? allExecuted < config.limit : true,
 | 
					 | 
				
			||||||
    ({ config, executed }) => !executed || config.repeat != null,
 | 
					 | 
				
			||||||
    ({ config }) => ['repeat', 'startsOn'].some(key => config[key]),
 | 
					 | 
				
			||||||
    function (workflow, now) {
 | 
					 | 
				
			||||||
      const { repeat } = workflow.config;
 | 
					 | 
				
			||||||
  if (typeof repeat !== 'string') {
 | 
					  if (typeof repeat !== 'string') {
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -356,12 +357,30 @@ export default class ScheduleTrigger extends Trigger {
 | 
				
			|||||||
  const interval = parser.parseExpression(repeat, { currentDate });
 | 
					  const interval = parser.parseExpression(repeat, { currentDate });
 | 
				
			||||||
  let next = interval.next();
 | 
					  let next = interval.next();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (next.getTime() === timestamp) {
 | 
					  // NOTE: cache all workflows will be matched in current cycle
 | 
				
			||||||
 | 
					  if (next.getTime() - timestamp <= range) {
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return false;
 | 
					  return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class ScheduleTrigger extends Trigger {
 | 
				
			||||||
 | 
					  static CacheRules = [
 | 
				
			||||||
 | 
					    ({ config, allExecuted }) => (config.limit ? allExecuted < config.limit : true) && config.startsOn,
 | 
				
			||||||
 | 
					    matchNext,
 | 
				
			||||||
 | 
					    function(workflow, now) {
 | 
				
			||||||
 | 
					      const { mode } = workflow.config;
 | 
				
			||||||
 | 
					      const modeHandlers = ScheduleModes.get(mode);
 | 
				
			||||||
 | 
					      return modeHandlers.shouldCache.call(this, workflow, now);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static TriggerRules = [
 | 
				
			||||||
 | 
					    ({ config, allExecuted }) => (config.limit ? allExecuted < config.limit : true) && config.startsOn,
 | 
				
			||||||
 | 
					    function (workflow, now) {
 | 
				
			||||||
 | 
					      return matchNext.call(this, workflow, now, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  events = new Map();
 | 
					  events = new Map();
 | 
				
			||||||
@ -386,7 +405,10 @@ export default class ScheduleTrigger extends Trigger {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  init() {
 | 
					  init() {
 | 
				
			||||||
    if (!this.timer) {
 | 
					    if (this.timer) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const now = new Date();
 | 
					    const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // NOTE: assign to this.timer to avoid duplicated initialization
 | 
					    // NOTE: assign to this.timer to avoid duplicated initialization
 | 
				
			||||||
@ -405,7 +427,6 @@ export default class ScheduleTrigger extends Trigger {
 | 
				
			|||||||
      1_000 - now.getMilliseconds()
 | 
					      1_000 - now.getMilliseconds()
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  run = () => {
 | 
					  run = () => {
 | 
				
			||||||
    const now = new Date();
 | 
					    const now = new Date();
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user