@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.
Files changed (2) hide show
  1. package/index.js +474 -0
  2. 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
+ }