@live-change/timer-service 0.2.29
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/index.js +474 -0
- package/package.json +27 -0
package/index.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
const app = require("@live-change/framework").app()
|
|
2
|
+
|
|
3
|
+
const definition = app.createServiceDefinition({
|
|
4
|
+
name: "timer"
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
let queueDuration = 1 * 60 * 1000
|
|
8
|
+
let loadMoreAfter = Math.floor(queueDuration / 2)
|
|
9
|
+
|
|
10
|
+
let timersQueue = []
|
|
11
|
+
let timersById = new Map()
|
|
12
|
+
let timersLoopStarted = false
|
|
13
|
+
let timersLoopTimeout = false
|
|
14
|
+
let lastLoadTime = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const Timer = definition.model({
|
|
18
|
+
name: "Timer",
|
|
19
|
+
properties: {
|
|
20
|
+
timestamp: {
|
|
21
|
+
type: Number
|
|
22
|
+
},
|
|
23
|
+
loops: {
|
|
24
|
+
type: Number
|
|
25
|
+
},
|
|
26
|
+
interval: {
|
|
27
|
+
type: Number
|
|
28
|
+
},
|
|
29
|
+
maxRetries: {
|
|
30
|
+
type: Number
|
|
31
|
+
},
|
|
32
|
+
retryDelay: {
|
|
33
|
+
type: Number
|
|
34
|
+
},
|
|
35
|
+
retries: {
|
|
36
|
+
type: Number
|
|
37
|
+
},
|
|
38
|
+
service: {
|
|
39
|
+
type: String
|
|
40
|
+
},
|
|
41
|
+
command: {
|
|
42
|
+
type: Object
|
|
43
|
+
},
|
|
44
|
+
trigger: {
|
|
45
|
+
type: Object
|
|
46
|
+
},
|
|
47
|
+
origin: {
|
|
48
|
+
type: Object
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
indexes: {
|
|
52
|
+
timestamp: {
|
|
53
|
+
property: 'timestamp',
|
|
54
|
+
function: async function(input, output) {
|
|
55
|
+
const mapper = (obj) => ({ id: (''+obj.timestamp).padStart(16, '0') + '_' + obj.id, to: obj.id })
|
|
56
|
+
await input.table('timer_Timer').onChange(
|
|
57
|
+
(obj, oldObj) => output.change(obj && mapper(obj), oldObj && mapper(oldObj))
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
function fireTimer(timer) {
|
|
66
|
+
runTimerAction(timer).catch(error => {
|
|
67
|
+
console.error("TIMER ACTION ERROR", error)
|
|
68
|
+
let timestamp = Date.now() + timer.retryDelay
|
|
69
|
+
timer.retries ++
|
|
70
|
+
if(timer.retries > timer.maxRetries) {
|
|
71
|
+
app.emitEvents("timer", [{
|
|
72
|
+
type: "timerFinished",
|
|
73
|
+
timer: timer.id,
|
|
74
|
+
error
|
|
75
|
+
}], { ...(timer.origin || {}), through: 'timer' })
|
|
76
|
+
timersById.delete(timer.id)
|
|
77
|
+
} else { // Retry
|
|
78
|
+
app.emitEvents("timer", [{
|
|
79
|
+
type: "timerFailed",
|
|
80
|
+
timer: timer.id,
|
|
81
|
+
error, timestamp,
|
|
82
|
+
retries: timer.retries
|
|
83
|
+
}], { ...(timer.origin || {}), through: 'timer' })
|
|
84
|
+
timer.timestamp = timestamp
|
|
85
|
+
insertTimer(timer)
|
|
86
|
+
}
|
|
87
|
+
}).then(done => {
|
|
88
|
+
console.error("TIMER ACTION FINISHED", done)
|
|
89
|
+
timer.loops --
|
|
90
|
+
if(timer.loops < 0) {
|
|
91
|
+
console.log("TIMER FINISHED")
|
|
92
|
+
app.emitEvents("timer",[{
|
|
93
|
+
type: "timerFinished",
|
|
94
|
+
timer: timer.id
|
|
95
|
+
}], { ...(timer.origin || {}), through: 'timer' })
|
|
96
|
+
timersById.delete(timer.id)
|
|
97
|
+
} else {
|
|
98
|
+
let timestamp = timer.timestamp + timer.interval
|
|
99
|
+
console.log("TIMER FIRED")
|
|
100
|
+
app.emitEvents("timer",[{
|
|
101
|
+
type: "timerFired",
|
|
102
|
+
timer: timer.id,
|
|
103
|
+
timestamp,
|
|
104
|
+
loops: timer.loops
|
|
105
|
+
}], { ...(timer.origin || {}), through: 'timer' })
|
|
106
|
+
timer.timestamp = timestamp
|
|
107
|
+
insertTimer(timer)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function timersLoop() {
|
|
113
|
+
//console.log("TL", timersQueue.length, timersQueue[0] && (timersQueue[0].timestamp - Date.now()))
|
|
114
|
+
timersLoopTimeout = false
|
|
115
|
+
if(timersQueue.length == 0) {
|
|
116
|
+
timersLoopStarted = false
|
|
117
|
+
setTimeout(checkIfThereIsMore, loadMoreAfter)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
let nextTs = timersQueue[0].timestamp
|
|
121
|
+
let now = Date.now()
|
|
122
|
+
while(nextTs < now) {
|
|
123
|
+
fireTimer(timersQueue.shift())
|
|
124
|
+
if(timersQueue.length == 0) {
|
|
125
|
+
timersLoopStarted = false
|
|
126
|
+
setTimeout(checkIfThereIsMore, loadMoreAfter)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
nextTs = timersQueue[0].timestamp
|
|
130
|
+
}
|
|
131
|
+
let delay = nextTs - Date.now()
|
|
132
|
+
if(delay > 1000) delay = 1000
|
|
133
|
+
await maybeLoadMore()
|
|
134
|
+
if(timersLoopTimeout === false) {
|
|
135
|
+
timersLoopTimeout = setTimeout(timersLoop, delay)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function startTimersLoop() {
|
|
140
|
+
timersLoopStarted = true
|
|
141
|
+
timersLoop()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resetTimersLoop() {
|
|
145
|
+
if(timersLoopTimeout !== false) clearTimeout(timersLoopTimeout)
|
|
146
|
+
timersLoop()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function appendTimers(timers) {
|
|
150
|
+
for(let timer of timers) {
|
|
151
|
+
if(!timersById.has(timer.id)) {
|
|
152
|
+
timersQueue.push(timer)
|
|
153
|
+
timersById.set(timer.id, timer)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function maybeLoadMore() {
|
|
159
|
+
if(!(lastLoadTime - Date.now() < loadMoreAfter)) return
|
|
160
|
+
let loadTime = Date.now() + queueDuration
|
|
161
|
+
let timers = await app.dao.get(['database', 'query', app.databaseName, `(${
|
|
162
|
+
async (input, output, { encodedFrom, encodedTo }) => {
|
|
163
|
+
const mapper = async (res) => input.table('timer_Timer').object(res.to).get()
|
|
164
|
+
await input.index('timer_Timer_timestamp').range({
|
|
165
|
+
gte: encodedFrom,
|
|
166
|
+
lt: encodedTo
|
|
167
|
+
}).onChange(async (obj, oldObj) => {
|
|
168
|
+
output.change(obj && await mapper(obj), oldObj && await mapper(oldObj))
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
})`, { encodedFrom: (''+lastLoadTime).padStart(16, '0')+'_', encodedTo: (''+loadTime).padStart(16, '0')+'_' }])
|
|
172
|
+
lastLoadTime = loadTime
|
|
173
|
+
for(let timer of timers) {
|
|
174
|
+
insertTimer(timer)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function checkIfThereIsMore() {
|
|
179
|
+
if(timersLoopStarted) return // loop started
|
|
180
|
+
let loadTime = Date.now() + queueDuration
|
|
181
|
+
//console.log("CHECK IF THERE IS MORE?", loadTime)
|
|
182
|
+
let timers = await app.dao.get(['database', 'query', app.databaseName, `(${
|
|
183
|
+
async (input, output, { encodedFrom, encodedTo }) => {
|
|
184
|
+
const mapper = async (res) => input.table('timer_Timer').object(res.to).get()
|
|
185
|
+
await input.index('timer_Timer_timestamp').range({
|
|
186
|
+
gte: encodedFrom,
|
|
187
|
+
lt: encodedTo
|
|
188
|
+
}).onChange(async (obj, oldObj) => {
|
|
189
|
+
output.change(obj && await mapper(obj), oldObj && await mapper(oldObj))
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
})`, { encodedFrom: '', encodedTo: (''+loadTime).padStart(16, '0')+'_' }])
|
|
193
|
+
if(timers.length == 0) {
|
|
194
|
+
//console.log("NO MORE")
|
|
195
|
+
if(!timersLoopStarted) setTimeout(checkIfThereIsMore, loadMoreAfter)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
lastLoadTime = loadTime
|
|
199
|
+
appendTimers(timers)
|
|
200
|
+
if(!timersLoopStarted) startTimersLoop()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async function startTimers() {
|
|
205
|
+
console.error("START TIMERS")
|
|
206
|
+
|
|
207
|
+
let loadTime = Date.now() + queueDuration
|
|
208
|
+
|
|
209
|
+
let timers = await app.dao.get(['database', 'query', app.databaseName, `(${
|
|
210
|
+
async (input, output, { encodedFrom, encodedTo }) => {
|
|
211
|
+
const mapper = async (res) => input.table('timer_Timer').object(res.to).get()
|
|
212
|
+
await input.index('timer_Timer_timestamp').range({
|
|
213
|
+
gte: encodedFrom,
|
|
214
|
+
lt: encodedTo
|
|
215
|
+
}).onChange(async (obj, oldObj) => {
|
|
216
|
+
output.change(obj && await mapper(obj), oldObj && await mapper(oldObj))
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
})`, { encodedFrom: '', encodedTo: (''+loadTime).padStart(16, '0')+'_' }])
|
|
220
|
+
console.error("NEXT TIMERS", timers)
|
|
221
|
+
lastLoadTime = loadTime
|
|
222
|
+
appendTimers(timers)
|
|
223
|
+
if(!timersLoopStarted) startTimersLoop()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function runTimerAction(timer) {
|
|
227
|
+
console.error("RUN ACTION", timer)
|
|
228
|
+
if(timer.command) {
|
|
229
|
+
if(!timer.command.service) timer.command.service = timer.service
|
|
230
|
+
return app.command({
|
|
231
|
+
...timer.command,
|
|
232
|
+
origin: { ...(timer.origin || {}), through: "timer" }
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
if(timer.trigger) {
|
|
236
|
+
if(timer.service) {
|
|
237
|
+
return app.triggerService(timer.service, {
|
|
238
|
+
...timer.trigger,
|
|
239
|
+
origin: { ...(timer.origin || {}), through: "timer" }
|
|
240
|
+
})
|
|
241
|
+
} else {
|
|
242
|
+
return app.trigger({
|
|
243
|
+
...timer.trigger,
|
|
244
|
+
origin: { ...(timer.origin || {}), through: "timer" }
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return new Promise((resolve, reject) => resolve(true))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function insertTimer(timer) {
|
|
252
|
+
console.log("INSERT TIMER", timer, "TO", timersQueue)
|
|
253
|
+
timersById.set(timer.id, timer)
|
|
254
|
+
for(let i = 0; i < timersQueue.length; i++) {
|
|
255
|
+
if(timer.timestamp < timersQueue[i].timestamp) {
|
|
256
|
+
timersQueue.splice(i, 0, timer)
|
|
257
|
+
if(i == 0) { // reset timers loop
|
|
258
|
+
resetTimersLoop()
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
timersQueue.push(timer)
|
|
264
|
+
if(!timersLoopStarted) {
|
|
265
|
+
startTimersLoop()
|
|
266
|
+
} else if(timer.timestamp - Date.now() < 1000) {
|
|
267
|
+
resetTimersLoop()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function removeTimer(timerId) {
|
|
272
|
+
for(let i = 0; i < timersQueue; i++) {
|
|
273
|
+
if(timersQueue[i].id == timerId) {
|
|
274
|
+
timersQueue.splice(i, 1)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
timersById.delete(timerId)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
definition.trigger({
|
|
281
|
+
name: "createTimer",
|
|
282
|
+
properties: {
|
|
283
|
+
timer: {
|
|
284
|
+
type: Object
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
async execute({ timer }, { client, service }, emit) {
|
|
288
|
+
console.log("CREATE TIMER", timer)
|
|
289
|
+
if(!timer) throw new Error("timer is required")
|
|
290
|
+
let timestamp = timer.timestamp
|
|
291
|
+
let loops = timer.loops || 0
|
|
292
|
+
let timerId = timer.id || app.generateUid()
|
|
293
|
+
let maxRetries = timer.maxRetries || 0
|
|
294
|
+
let retryDelay = timer.retryDelay || 5 * 1000
|
|
295
|
+
let interval = timer.interval || 0
|
|
296
|
+
if(loops > 0 && interval == 0) throw new Error("impossibleTimer")
|
|
297
|
+
const props = {
|
|
298
|
+
...timer, timestamp, loops, interval, timerId, maxRetries, retryDelay, retries: 0
|
|
299
|
+
}
|
|
300
|
+
emit({
|
|
301
|
+
type: "timerCreated",
|
|
302
|
+
timer: timerId,
|
|
303
|
+
data: props
|
|
304
|
+
})
|
|
305
|
+
if(timestamp < Date.now() + queueDuration) {
|
|
306
|
+
insertTimer({ ...props , id: timerId })
|
|
307
|
+
}
|
|
308
|
+
return timerId
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
definition.trigger({
|
|
314
|
+
name: "cancelTimer",
|
|
315
|
+
properties: {
|
|
316
|
+
timer: {
|
|
317
|
+
type: Timer
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
async execute({ timer }, { client, service }, emit) {
|
|
321
|
+
if(!timer) throw new Error("timer is required")
|
|
322
|
+
let timerRow = await Timer.get(timer)
|
|
323
|
+
if(!timerRow) throw 'notFound'
|
|
324
|
+
emit({
|
|
325
|
+
type: "timerCanceled", timer
|
|
326
|
+
})
|
|
327
|
+
removeTimer(timer)
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
definition.trigger({
|
|
333
|
+
name: "cancelTimerIfExists",
|
|
334
|
+
properties: {
|
|
335
|
+
timer: {
|
|
336
|
+
type: Timer
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
async execute({ timer }, { client, service }, emit) {
|
|
340
|
+
if(!timer) throw new Error("timer is required")
|
|
341
|
+
let timerRow = await Timer.get(timer)
|
|
342
|
+
if(!timerRow) return false
|
|
343
|
+
emit({
|
|
344
|
+
type: "timerCanceled", timer
|
|
345
|
+
})
|
|
346
|
+
removeTimer(timer)
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
definition.action({
|
|
353
|
+
name: "create",
|
|
354
|
+
properties: {
|
|
355
|
+
timer: {
|
|
356
|
+
type: Object
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
async execute({ timer }, { client, service }, emit) {
|
|
360
|
+
if(!timer) throw new Error("timer is required")
|
|
361
|
+
let timestamp = timer.timestamp
|
|
362
|
+
let loops = timer.loops || 0
|
|
363
|
+
let timerId = timer.id || app.generateUid()
|
|
364
|
+
let maxRetries = timer.maxRetries || 0
|
|
365
|
+
let retryDelay = timer.retryDelay || 5 * 1000
|
|
366
|
+
let interval = timer.interval || 0
|
|
367
|
+
if(loops > 0 && interval == 0) throw new Error("impossibleTimer")
|
|
368
|
+
const props = {
|
|
369
|
+
...timer, timestamp, loops, interval, timerId, maxRetries, retryDelay, retries: 0
|
|
370
|
+
}
|
|
371
|
+
emit({
|
|
372
|
+
type: "timerCreated",
|
|
373
|
+
timer: timerId,
|
|
374
|
+
data: props
|
|
375
|
+
})
|
|
376
|
+
if(timestamp < Date.now() + queueDuration) {
|
|
377
|
+
insertTimer({ ...props , id: timerId })
|
|
378
|
+
}
|
|
379
|
+
return timerId
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
definition.action({
|
|
385
|
+
name: "cancel",
|
|
386
|
+
properties: {
|
|
387
|
+
timer: {
|
|
388
|
+
type: Timer
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
async execute({ timer }, { client, service }, emit) {
|
|
392
|
+
if(!timer) throw new Error("timer is required")
|
|
393
|
+
let timerRow = await Timer.get(timer)
|
|
394
|
+
if(!timerRow) throw 'notFound'
|
|
395
|
+
emit({
|
|
396
|
+
type: "timerCanceled", timer
|
|
397
|
+
})
|
|
398
|
+
removeTimer(timer)
|
|
399
|
+
return true
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
definition.action({
|
|
404
|
+
name: "cancelIfExists",
|
|
405
|
+
properties: {
|
|
406
|
+
timer: {
|
|
407
|
+
type: Timer
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
async execute({ timer }, { client, service }, emit) {
|
|
411
|
+
if(!timer) throw new Error("timer is required")
|
|
412
|
+
let timerRow = await Timer.get(timer)
|
|
413
|
+
if(!timerRow) return false
|
|
414
|
+
emit({
|
|
415
|
+
type: "timerCanceled", timer
|
|
416
|
+
})
|
|
417
|
+
removeTimer(timer)
|
|
418
|
+
return true
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
definition.event({
|
|
423
|
+
queuedBy: 'timer',
|
|
424
|
+
name: "timerCreated",
|
|
425
|
+
async execute({ timer, data, origin }) {
|
|
426
|
+
return Timer.create({
|
|
427
|
+
id: timer,
|
|
428
|
+
...data,
|
|
429
|
+
origin: { ...origin, ...( data.origin || {} ) }
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
definition.event({
|
|
435
|
+
queuedBy: 'timer',
|
|
436
|
+
name: "timerCanceled",
|
|
437
|
+
async execute({ timer }) {
|
|
438
|
+
return Timer.delete(timer)
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
definition.event({
|
|
443
|
+
queuedBy: 'timer',
|
|
444
|
+
name: "timerFinished",
|
|
445
|
+
async execute({ timer }) {
|
|
446
|
+
return Timer.delete(timer)
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
definition.event({
|
|
451
|
+
queuedBy: 'timer',
|
|
452
|
+
name: "timerFired",
|
|
453
|
+
async execute({ timer, timestamp, loops }) {
|
|
454
|
+
return Timer.update(timer, {
|
|
455
|
+
loops, timestamp
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
definition.event({
|
|
461
|
+
queuedBy: 'timer',
|
|
462
|
+
name: "timerFailed",
|
|
463
|
+
async execute({ timer, timestamp, retries }) {
|
|
464
|
+
return Timer.update(timer, {
|
|
465
|
+
retries, timestamp
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
definition.beforeStart(async () => {
|
|
471
|
+
await startTimers()
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
module.exports = definition
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@live-change/timer-service",
|
|
3
|
+
"version": "0.2.29",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "NODE_ENV=test tape tests/*"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/live-change/live-change-services.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/live-change/live-change-services/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/live-change/live-change-services",
|
|
18
|
+
"author": {
|
|
19
|
+
"email": "michal@laszczewski.pl",
|
|
20
|
+
"name": "Michał Łaszczewski",
|
|
21
|
+
"url": "https://www.viamage.com/"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@live-change/framework": "0.6.3"
|
|
25
|
+
},
|
|
26
|
+
"gitHead": "7691357c7569a18373668691eb6a81a9e161194d"
|
|
27
|
+
}
|