UNPKG

@darlean/timers-suite

Version:

Timers Suite that provides persistent timer functionality

195 lines (194 loc) 8.1 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TimerActor = exports.TIMER_MOMENT_INDEX = void 0; const base_1 = require("@darlean/base"); const utils_1 = require("@darlean/utils"); exports.TIMER_MOMENT_INDEX = 'by-moment'; class TimerActor { constructor(persistence, time, timer, portal) { this.persistence = persistence; this.time = time; this.timer = timer; this.portal = portal; } async activate() { this.timerHandle = this.timer.repeat(this.step, 60 * 1000, 0); } async deactivate() { this.timerHandle?.cancel(); } async schedule(options) { const newState = { id: options.id, nextMoment: 0, remainingRepeatCount: options.triggers[0].repeatCount ?? 1, remainingTriggers: options.triggers.map((x) => ({ ...x })), callbackActorType: options.callbackActorType, callbackActorId: options.callbackActorId, callbackActionName: options.callbackActionName, callbackActionArgs: [...(options.callbackActionArgs ?? [])] }; const newMoment = this.updateNextMoment(newState); if (!newMoment) { throw new Error('Unable to schedule timer'); } const item = await this.persistence.load([newState.id]); item.change(newState); await item.persist(); if (this.nextTime !== undefined) { if (newState.nextMoment <= this.nextTime) { this.nextTime = newState.nextMoment; this.trigger(); } } else { this.nextTime = newState.nextMoment; this.trigger(); } } // When cancelling and a step is performed meanwhile, the cancel can delete the record and the step restore it. // To prevent this, make cancel exclusive. We may implement a better (smarter) solution, but for now it is reasonable. async cancel(options) { const item = await this.persistence.load([options.id]); if (item.hasValue()) { item.clear(); await item.persist(); } } async touch() { // } async step() { const now = this.time.machineTime(); const options = { index: exports.TIMER_MOMENT_INDEX, keys: [{ operator: 'lte', value: (0, utils_1.encodeNumber)(now) }] }; for await (const chunk of this.persistence.searchChunks(options)) { // We are the only actor that is processing the item (we are a singleton). // No need to "reserve" the item; when we fail/crash, another actor will retry // (but we are then dead anyways) // TODO: Process items in parallel for (const item of chunk.items) { const currentItem = await this.persistence.load(item.id); const currentTimer = currentItem.tryGetValue(); if (!currentTimer) { continue; } let breaking = false; const trigger = currentTimer.remainingTriggers[0]; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const actor = this.portal.retrieve(currentTimer.callbackActorType, currentTimer.callbackActorId); await actor[currentTimer.callbackActionName](); if ((trigger?.success ?? 'continue') === 'break') { breaking = true; } } catch (e) { if ((trigger?.error ?? 'continue') === 'break') { breaking = true; } } finally { if (!breaking) { const newMoment = this.updateNextMoment(currentTimer); if (newMoment === undefined) { breaking = true; } } if (breaking) { currentItem.clear(); } await currentItem.persist('always'); } } if (chunk.items.length > 0) { // When we did process some items, do not continue with next chunk (because new items may have come in between) // but redo the entire step. if (chunk.continuationToken) { this.timerHandle?.resume(0); return; } break; } } this.nextTime = undefined; // Note: In between, parallel requests to 'schedule' may adjust nextTime const options2 = { index: exports.TIMER_MOMENT_INDEX, maxChunkItems: 1 }; for await (const item of this.persistence.searchItems(options2)) { if (item.keys?.[0]) { const time = (0, utils_1.decodeNumber)(item.keys[0]); if (this.nextTime === undefined || time < this.nextTime) { this.nextTime = time; this.trigger(); } } break; } } trigger() { if (this.nextTime) { const now = this.time.machineTime(); const remaining = Math.max(0, this.nextTime - now); this.timerHandle?.resume(Math.min(60 * 1000, remaining)); } } updateNextMoment(state) { let trigger = state.remainingTriggers[0]; const now = this.time.machineTime(); if (trigger) { if (trigger.repeatCount === 0) { state.nextMoment = trigger.moment ?? now + (trigger.interval ?? 0); } else if (state.remainingRepeatCount > 0) { state.remainingRepeatCount--; state.nextMoment = trigger.moment ?? now + (trigger.interval ?? 0); } else { // No more remaining repeats, move to next trigger state.remainingTriggers = state.remainingTriggers.slice(1); trigger = state.remainingTriggers[0]; if (trigger) { state.remainingRepeatCount = (trigger.repeatCount ?? 1) - 1; state.nextMoment = trigger.moment ?? now + (trigger.interval ?? 0); } else { state.nextMoment = 0; return undefined; } } if (trigger.jitter ?? 0 > 0) { const offset = Math.random() * (trigger.jitter ?? 0); state.nextMoment += offset; } return state.nextMoment; } else { state.nextMoment = 0; return undefined; } } } __decorate([ (0, base_1.action)({ locking: 'shared' }) ], TimerActor.prototype, "schedule", null); __decorate([ (0, base_1.action)({ locking: 'exclusive' }) ], TimerActor.prototype, "cancel", null); __decorate([ (0, base_1.action)() ], TimerActor.prototype, "touch", null); __decorate([ (0, base_1.timer)({ locking: 'shared' }) ], TimerActor.prototype, "step", null); exports.TimerActor = TimerActor;