@live-change/cron-service 0.9.163 → 0.9.164

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/config.js CHANGED
@@ -2,12 +2,14 @@ import definition from './definition.js'
2
2
 
3
3
  const {
4
4
  adminRoles = ['admin', 'owner'],
5
+ readerRoles = ['reader'],
5
6
  ownerTypes = ['user_User'],
6
- topicTypes = ['topic_Topic']
7
+ topicTypes = undefined
7
8
  } = definition.config
8
9
 
9
10
  const config = {
10
11
  adminRoles,
12
+ readerRoles,
11
13
  ownerTypes,
12
14
  topicTypes
13
15
  }
package/interval.js CHANGED
@@ -3,7 +3,7 @@ const app = App.app()
3
3
 
4
4
  import definition from './definition.js'
5
5
  import config from './config.js'
6
- import { triggerType } from './run.js'
6
+ import { triggerType, runTrigger, doRunTrigger, waitForTasks, RunState } from './run.js'
7
7
 
8
8
  export const Interval = definition.model({
9
9
  name: "Interval",
@@ -15,31 +15,85 @@ export const Interval = definition.model({
15
15
  roles: config.adminRoles
16
16
  },
17
17
  readAccessControl: {
18
- roles: config.adminRoles
18
+ roles: [...config.adminRoles, ...config.readerRoles]
19
19
  },
20
20
  readAllAccess: ['admin'],
21
21
  },
22
22
  properties: {
23
23
  description: {
24
24
  type: String,
25
- description: "Description of the interval"
25
+ description: "Description of the interval",
26
+ input: 'textarea',
26
27
  },
27
28
  interval: {
28
29
  type: Number, // milliseconds
29
- description: "Interval between triggers in milliseconds"
30
+ description: "Interval between triggers in milliseconds",
31
+ validation: ['nonEmpty', 'integer'],
32
+ input: 'integer',
33
+ inputConfig: {
34
+ attributes: {
35
+ suffix: ' ms',
36
+ showButtons: true,
37
+ step: 1000,
38
+ min: 1000,
39
+ }
40
+ }
41
+ },
42
+ firstRunDelay: {
43
+ type: Number, // milliseconds
44
+ description: "Delay before first run",
45
+ input: 'integer',
46
+ inputConfig: {
47
+ attributes: {
48
+ suffix: ' ms',
49
+ showButtons: true,
50
+ step: 1000,
51
+ min: 0,
52
+ }
53
+ }
30
54
  },
31
55
  wait: {
32
56
  type: Number, // milliseconds
33
- description: "Wait for previous trigger to finish before planning next trigger"
57
+ description: "Wait for previous trigger to finish before planning next trigger",
58
+ input: 'integer',
59
+ inputConfig: {
60
+ attributes: {
61
+ suffix: ' ms',
62
+ showButtons: true,
63
+ step: 1000,
64
+ min: 0,
65
+ }
66
+ }
34
67
  },
35
68
  trigger: triggerType
36
69
  }
37
70
  })
38
71
 
39
- async function processInterval({ id, interval, wait, trigger }) {
72
+ export const IntervalInfo = definition.model({
73
+ name: "IntervalInfo",
74
+ propertyOf: {
75
+ what: Interval,
76
+ readAccessControl: {
77
+ roles: [...config.adminRoles, ...config.readerRoles]
78
+ }
79
+ },
80
+ properties: {
81
+ lastRun: {
82
+ type: Date
83
+ },
84
+ nextRun: {
85
+ type: Date
86
+ }
87
+ }
88
+ })
89
+
90
+ async function processInterval({ id, interval, wait, trigger, firstRunDelay, isFirstRun }, { triggerService }) {
91
+ //console.log("PROCESSING INTERVAL", id, interval, wait, trigger, firstRunDelay, isFirstRun)
40
92
  if(wait) await waitForTasks('cron_Interval', id)
41
- const nextTimestamp = Date.now() + interval
93
+ //console.log("WAIT FOR TASKS DONE", id)
94
+ const nextTimestamp = Date.now() + (isFirstRun ? (firstRunDelay || 0) : interval)
42
95
  const nextTime = new Date(nextTimestamp)
96
+ //console.log("NEXT TIME", nextTime)
43
97
  await triggerService({
44
98
  service: 'timer',
45
99
  type: 'createTimer',
@@ -59,6 +113,11 @@ async function processInterval({ id, interval, wait, trigger }) {
59
113
  }
60
114
  }
61
115
  })
116
+ await IntervalInfo.update(id, {
117
+ id,
118
+ nextRun: nextTime
119
+ })
120
+ //console.log("TIMER CREATED", id)
62
121
  }
63
122
 
64
123
 
@@ -73,22 +132,34 @@ definition.trigger({
73
132
  const intervalData = await Interval.get(interval)
74
133
  if(!intervalData) return /// interval was deleted
75
134
  if(intervalData.wait) {
135
+ const lastRun = new Date()
76
136
  await runTrigger(intervalData.trigger, {
77
137
  trigger,
78
138
  triggerService,
79
139
  jobType: 'cron_Interval',
80
140
  job: interval,
81
- timeout: Number.isInteger(intervalData.wait) ? intervalData.wait : undefined
141
+ timeout: Number.isInteger(intervalData.wait) ? intervalData.wait : undefined,
142
+ runTime: lastRun
82
143
  })
83
- await processInterval(intervalData)
144
+ await IntervalInfo.update(interval, {
145
+ id: interval,
146
+ lastRun
147
+ })
148
+ await processInterval(intervalData, { triggerService })
84
149
  } else {
85
- await processInterval(intervalData) // no wait, process immediately
150
+ await processInterval(intervalData, { triggerService }) // no wait, process immediately
86
151
  try {
152
+ const lastRun = new Date()
87
153
  await doRunTrigger(intervalData.trigger, {
88
154
  trigger,
89
155
  triggerService,
90
156
  jobType: 'cron_Interval',
91
- job: interval
157
+ job: interval,
158
+ runTime: lastRun
159
+ })
160
+ await IntervalInfo.update(interval, {
161
+ id: interval,
162
+ lastRun
92
163
  })
93
164
  } catch(error) {
94
165
  console.error("ERROR RUNNING INTERVAL TRIGGER", error)
@@ -116,27 +187,28 @@ definition.trigger({
116
187
  if(oldData) { // clear old version
117
188
  await triggerService({
118
189
  service: 'timer',
119
- type: 'cancelTimerIfExists',
120
- data: {
121
- timer: 'cron_Interval_' + object.id
122
- }
190
+ type: 'cancelTimerIfExists',
191
+ }, {
192
+ timer: 'cron_Interval_' + object
123
193
  })
124
194
  if(!data) { // full cleanup
125
195
  await triggerService({
126
196
  service: 'timer',
127
- type: 'cancelTimerIfExists',
128
- data: {
129
- timer: 'cron_run_timeout_' + object.id
130
- }
197
+ type: 'cancelTimerIfExists',
198
+ }, {
199
+ timer: 'cron_run_timeout_' + object
131
200
  })
132
- RunState.delete(App.encodeIdentifier(['cron_Interval', object.id]))
201
+ RunState.delete(App.encodeIdentifier(['cron_Interval', object]))
133
202
  }
134
203
  }
135
204
  if(data) { // setup new version
205
+ console.log("PROCESSING INTERVAL", object, data)
136
206
  await processInterval({
137
- id: object,
138
- ...data``
139
- })
207
+ id: object,
208
+ ...data,
209
+ isFirstRun: !oldData
210
+ }, { triggerService })
211
+ console.log("PROCESSING INTERVAL DONE", object, data)
140
212
  }
141
213
  }
142
214
  })
@@ -156,7 +228,14 @@ definition.afterStart(async (service) => {
156
228
  const existingTimer = await Timer.get('cron_Interval_' + interval.id)
157
229
  if(!existingTimer) {
158
230
  console.error("INTERVAL", interval, "HAS NO TIMER, REPROCESSING")
159
- await processInterval(interval)
231
+ await processInterval(interval, {
232
+ triggerService: (trigger, data, returnArray = false) =>
233
+ app.triggerService({
234
+ ...trigger,
235
+ causeType: 'cron_Interval',
236
+ cause: 'cron_Interval_' + interval.id,
237
+ }, data, returnArray)
238
+ })
160
239
  }
161
240
  }
162
241
  } while(bucket.length === bucketSize)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/cron-service",
3
- "version": "0.9.163",
3
+ "version": "0.9.164",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@live-change/framework": "^0.9.163"
25
+ "@live-change/framework": "^0.9.164"
26
26
  },
27
- "gitHead": "c410e1dacd07daed9a5c55367abd7f19d9a575cc"
27
+ "gitHead": "38f9fb8b01a9527d8f6036e174edd1fa41443301"
28
28
  }
package/run.js CHANGED
@@ -4,12 +4,16 @@ const app = App.app()
4
4
  import definition from './definition.js'
5
5
 
6
6
  const Task = definition.foreignModel('task', 'Task')
7
+ import config from './config.js'
7
8
 
8
9
  export const RunState = definition.model({
9
10
  name: "RunState",
10
11
  propertyOfAny: {
11
12
  to: ['job'],
12
- jobTypes: ['cron_Interval', 'cron_Schedule']
13
+ jobTypes: ['cron_Interval', 'cron_Schedule'],
14
+ readAccessControl: {
15
+ roles: [...config.adminRoles, ...config.readerRoles]
16
+ }
13
17
  },
14
18
  properties: {
15
19
  state: {
@@ -37,43 +41,57 @@ export const RunState = definition.model({
37
41
  }
38
42
  })
39
43
 
40
-
41
44
  export const triggerType = {
42
45
  description: "Trigger to schedule",
43
- input: 'triggerEditor',
46
+ input: 'trigger',
44
47
  type: Object,
45
48
  properties: {
46
49
  name: {
47
50
  description: "Trigger name",
48
51
  type: String,
52
+ validation: ['nonEmpty']
49
53
  },
50
54
  service: {
51
55
  description: "Service holding the trigger, or null for triggering any service",
52
56
  type: String,
57
+ validation: ['nonEmpty']
53
58
  },
54
59
  properties: {
55
60
  description: "Properties to pass to the trigger",
56
- type: Object,
61
+ type: Object,
62
+ input: 'json',
63
+ defaultValue: {}
64
+ },
65
+ returnTask: {
66
+ description: "Return task to wait for",
67
+ type: Boolean,
68
+ defaultValue: false
57
69
  }
58
70
  }
59
71
  }
60
72
 
61
- export async function doRunTrigger(triggerInfo, { trigger, triggerService, jobType, job }) {
73
+ export async function doRunTrigger(triggerInfo, { trigger, triggerService, jobType, job, runTime }) {
62
74
  if(triggerInfo.service) {
63
75
  return await triggerService({
64
76
  type: triggerInfo.name,
65
77
  service: triggerInfo.service,
66
78
  causeType: jobType, cause: job
67
- }, triggerInfo.properties, true)
79
+ }, {
80
+ ...triggerInfo.properties,
81
+ jobRunTime: runTime
82
+ }, true)
68
83
  } else {
69
84
  return await trigger({
70
85
  type: triggerInfo.name,
71
86
  causeType: jobType, cause: job
72
- }, triggerInfo.properties)
87
+ }, {
88
+ ...triggerInfo.properties,
89
+ jobRunTime: runTime
90
+ })
73
91
  }
74
92
  }
75
93
 
76
- export async function runTrigger(triggerInfo, { trigger, triggerService, jobType, job, timeout }) {
94
+ export async function runTrigger(triggerInfo, { trigger, triggerService, jobType, job, timeout, runTime }) {
77
95
  const id = App.encodeIdentifier([jobType, job])
78
96
  await RunState.create({
79
97
  id,
@@ -104,13 +122,13 @@ export async function runTrigger(triggerInfo, { trigger, triggerService, jobType
104
122
  }
105
123
  let triggerResults = []
106
124
  try {
107
- triggerResults = await doRunTrigger(triggerInfo, { trigger, triggerService, jobType, job })
125
+ triggerResults = await doRunTrigger(triggerInfo, { trigger, triggerService, jobType, job, runTime })
108
126
  } catch(error) {
109
127
  console.error("ERROR RUNNING TRIGGER", error)
110
128
  /// Ignore error, it will be handled by the task service
111
129
  }
112
130
  if(triggerInfo.returnTask) {
113
- const tasks = await Promise.all(triggerResults.map((result) => Task.get(result)))
131
+ const tasks = (await Promise.all(triggerResults.map((result) => Task.get(result))))
114
132
  .filter(task => task !== undefined)
115
133
  .filter(task => task.state !== 'done' && task.state !== 'failed')
116
134
  if(tasks.length > 0) {
@@ -185,7 +203,7 @@ export async function waitForTasks(jobType, job) {
185
203
  }
186
204
  const runStateObservable = RunState.observable(runState)
187
205
  const runStateObserver = {
188
- set: (value) => {
206
+ set: (value) => {
189
207
  if(!value) finish()
190
208
  if(value.tasks) {
191
209
  for(const taskId of value.tasks) {
@@ -194,6 +212,7 @@ export async function waitForTasks(jobType, job) {
194
212
  }
195
213
  }
196
214
  }
215
+ runStateObservable.observe(runStateObserver)
197
216
  function finish() {
198
217
  if(done) return
199
218
  done = true
package/schedule.js CHANGED
@@ -3,7 +3,7 @@ const app = App.app()
3
3
 
4
4
  import definition from './definition.js'
5
5
  import config from './config.js'
6
- import { triggerType } from './run.js'
6
+ import { triggerType, runTrigger, doRunTrigger, waitForTasks } from './run.js'
7
7
 
8
8
  export const Schedule = definition.model({
9
9
  name: "Schedule",
@@ -21,30 +21,104 @@ export const Schedule = definition.model({
21
21
  },
22
22
  properties: {
23
23
  description: {
24
- type: String
24
+ type: String,
25
+ description: "Description of the schedule",
26
+ input: 'textarea',
25
27
  },
26
28
  minute: {
27
- type: Number // NaN for every minute
29
+ type: Number, // NaN for every minute
30
+ description: "Minute of the hour to run the schedule",
31
+ input: 'integer',
32
+ inputConfig: {
33
+ attributes: {
34
+ showButtons: true,
35
+ step: 1,
36
+ min: 0,
37
+ max: 59,
38
+ }
39
+ }
28
40
  },
29
41
  hour: {
30
42
  type: Number, // NaN for every hour
43
+ description: "Hour of the day to run the schedule",
44
+ input: 'integer',
45
+ inputConfig: {
46
+ attributes: {
47
+ showButtons: true,
48
+ step: 1,
49
+ min: 0,
50
+ max: 23,
51
+ }
52
+ }
31
53
  },
32
54
  day: {
33
55
  type: Number, // NaN for every day
56
+ description: "Day of the month to run the schedule",
57
+ input: 'integer',
58
+ inputConfig: {
59
+ attributes: {
60
+ showButtons: true,
61
+ step: 1,
62
+ min: 1,
63
+ max: 31,
64
+ }
65
+ }
34
66
  },
35
67
  dayOfWeek: {
36
68
  type: Number, // NaN for every day of week
69
+ description: "Day of the week to run the schedule",
70
+ input: 'integer',
71
+ inputConfig: {
72
+ attributes: {
73
+ showButtons: true,
74
+ step: 1,
75
+ min: 1,
76
+ max: 7,
77
+ }
78
+ }
37
79
  },
38
80
  month: {
39
81
  type: Number, // NaN for every month
82
+ description: "Month of the year to run the schedule",
83
+ input: 'integer',
84
+ inputConfig: {
85
+ attributes: {
86
+ showButtons: true,
87
+ step: 1,
88
+ min: 1,
89
+ max: 12,
90
+ }
91
+ }
40
92
  },
41
93
  trigger: triggerType
42
94
  }
43
95
  })
44
96
 
97
+ export const ScheduleInfo = definition.model({
98
+ name: "ScheduleInfo",
99
+ propertyOf: {
100
+ what: Schedule,
101
+ readAccessControl: {
102
+ roles: [...config.adminRoles, ...config.readerRoles]
103
+ }
104
+ },
105
+ properties: {
106
+ lastRun: {
107
+ type: Date
108
+ },
109
+ nextRun: {
110
+ type: Date
111
+ }
112
+ }
113
+ })
114
+
45
115
  function getNextTime(schedule) {
46
116
  const now = new Date()
47
117
  let time = now
118
+
119
+ /// set to next minute
120
+ time = new Date(time.getFullYear(), time.getMonth(), time.getDate(), time.getHours(), time.getMinutes() + 1, 0)
121
+
48
122
  if(Number.isInteger(schedule.minute)) {
49
123
  if(time.getMinutes() >= schedule.minute) { // next hour
50
124
  time = new Date(time.getFullYear(), time.getMonth(), time.getDate(), time.getHours() + 1, schedule.minute, 0)
@@ -72,8 +146,8 @@ function getNextTime(schedule) {
72
146
  return time
73
147
  }
74
148
 
75
- async function processSchedule({ id, minute, hour, day, dayOfWeek, month, trigger }) {
76
- const nextTime = getNextTime(schedule)
149
+ async function processSchedule({ id, minute, hour, day, dayOfWeek, month, trigger }, { triggerService }) {
150
+ const nextTime = getNextTime({ minute, hour, day, dayOfWeek, month })
77
151
  const nextTimestamp = nextTime.getTime()
78
152
  await triggerService({
79
153
  service: 'timer',
@@ -94,6 +168,10 @@ async function processSchedule({ id, minute, hour, day, dayOfWeek, month, trigge
94
168
  }
95
169
  }
96
170
  })
171
+ await ScheduleInfo.update(id, {
172
+ id,
173
+ nextRun: nextTime
174
+ })
97
175
  }
98
176
 
99
177
  definition.trigger({
@@ -106,18 +184,23 @@ definition.trigger({
106
184
  execute: async ({ schedule }, { service, trigger, triggerService }, emit) => {
107
185
  const scheduleData = await Schedule.get(schedule)
108
186
  if(!scheduleData) return /// schedule was deleted
109
- await processSchedule(scheduleData) // no wait, process immediately
187
+ await processSchedule(scheduleData, { triggerService }) // no wait, process immediately
110
188
  try {
189
+ const lastRun = new Date()
111
190
  await doRunTrigger(scheduleData.trigger, {
112
191
  trigger,
113
192
  triggerService,
114
193
  jobType: 'cron_Schedule',
115
- job: schedule
194
+ job: schedule,
195
+ runTime: lastRun
196
+ })
197
+ await ScheduleInfo.update(schedule, {
198
+ id: schedule,
199
+ lastRun
116
200
  })
117
201
  } catch(error) {
118
202
  console.error("ERROR RUNNING SCHEDULE TRIGGER", error)
119
203
  }
120
-
121
204
  }
122
205
  })
123
206
 
@@ -140,16 +223,16 @@ definition.trigger({
140
223
  await triggerService({
141
224
  service: 'timer',
142
225
  type: 'cancelTimerIfExists',
143
- data: {
144
- timer: 'cron_Schedule_' + object.id
145
- }
226
+ }, {
227
+ timer: 'cron_Schedule_' + object
146
228
  })
229
+ await ScheduleInfo.delete(object)
147
230
  }
148
231
  if(data) {
149
232
  await processSchedule({
150
233
  id: object,
151
234
  ...data
152
- })
235
+ }, { triggerService })
153
236
  }
154
237
  }
155
238
  })
@@ -170,7 +253,14 @@ definition.afterStart(async (service) => {
170
253
  const existingTimer = await Timer.get('cron_Schedule_' + schedule.id)
171
254
  if(!existingTimer) {
172
255
  console.error("SCHEDULE", schedule, "HAS NO TIMER, REPROCESSING")
173
- await processSchedule(schedule)
256
+ await processSchedule(schedule, {
257
+ triggerService: (trigger, data, returnArray = false) =>
258
+ app.triggerService({
259
+ ...trigger,
260
+ causeType: 'cron_Schedule',
261
+ cause: 'cron_Schedule_' + schedule.id,
262
+ }, data, returnArray)
263
+ })
174
264
  }
175
265
  }
176
266
  })