UNPKG

@jovian/type-tools

Version:

TypeTools is a Typescript library for providing extensible tooling runtime validations and type helpers.

986 lines (960 loc) 34.5 kB
/* Jovian (c) 2020, License: MIT */ import { ixConfig } from './ix.config'; import { Entity } from './ix.entity'; import { errorCast } from './ix.error'; import { BoolLike } from './util/options.util'; import { promise, PromUtil } from './util/prom.util'; interface TimerIntervalBehavior<T = any> { data?: any; begun?: boolean; callback?: (bh: TimerIntervalBehavior<T>, reg: TimerRegistrant<T>) => any; } interface SharedTimerRegistration { reg: TimerRegistrant; once?: boolean; } interface TimerUnitTime { ms?: number; s?: number; m?: number; h?: number; d?: number; wk?: number; mo?: number; yr?: number; } interface RetryUntilResultOptions { backoff?: { type?: 'linear' | 'expo'; start?: number; value?: number; max?: number; }; } interface BackOffType { backoff: 'default' | 'expo' | 'linear'; } export interface TimerOptions extends TimerUnitTime { /** UNIX Timestamp in ms */ t?: number; name?: string; mode?: string; nowait?: BoolLike; endSharp?: BoolLike; defer?: BoolLike; forever?: BoolLike; count?: number; immediately?: BoolLike; startNow?: BoolLike; } export interface TimerTickSnapshot<T> { now: number; result: T; context: string; maxCount: number; count: number; nth: number; ordinal: number; elapsed: number; timer: Timer; error?: Error; } export class SharedTimerBlock extends Entity { name: string = ''; active = true; started = false; startTime = 0; maxBlocks = 10000; // 10 seconds blockMs = 1; // 1 slot = 1 ms procCount = 1; procInterval = 1; controller: SharedTimer; replacedBy: SharedTimerBlock; replacing: SharedTimerBlock; lastRegister = 0; pendingCount = 0; notMsBlock = false; checkingLogic: (immediatesOnly?: boolean) => any; private procs: any[] = []; private immediates: SharedTimerRegistration[] = []; private timeblock: SharedTimerRegistration[][] = []; private timeblockCursor = 0; private maxMs = this.maxBlocks * this.blockMs; private last = 0; private totalHandledBlocksCount = 0; constructor(init?: Partial<SharedTimerBlock>) { super('timerblock'); if (init) { Object.assign(this, init); } this.addOnDestroy(async () => { this.clearAllProcs(); }); this.maxMs = this.maxBlocks * this.blockMs; this.notMsBlock = (this.blockMs > 1); } get maxSupportedMs() { return this.maxMs; } get totalHandledBlocks() { return this.totalHandledBlocksCount; } clearAllProcs() { this.checkingLogic(true); // clear out all immediates for (const proc of this.procs) { clearInterval(proc); } } setImmediate(reg: TimerRegistrant) { if (!reg.regStart) { reg.regStart = Date.now(); } if (!this.replacedBy) { const now = Date.now(); this.lastRegister = now; this.immediates.push({reg}); } else { this.getReplacement().setImmediate(reg); } } getReplacement() { let rpl = this.replacedBy; while (rpl.replacedBy) { rpl = rpl.replacedBy; } return rpl; } setImmediateOnce(reg: TimerRegistrant) { if (!reg.regStart) { reg.regStart = Date.now(); } if (!this.controller.started) { this.controller.startBlocks(); } if (!this.replacedBy) { const now = Date.now(); this.lastRegister = now; this.immediates.push({reg, once: true}); } else { const rpl = this.getReplacement(); rpl.immediates.push({reg, once: true}); } } setIntoBlock(reg: TimerRegistrant) { if (!this.replacedBy) { const now = Date.now(); this.lastRegister = now; ++this.pendingCount; if (this.notMsBlock) { let adjusted = reg.blockInterval; adjusted += this.timeblockCursor; while (adjusted >= this.timeblock.length) { adjusted -= this.timeblock.length; } const spot = this.timeblock[adjusted]; spot.push({reg}); reg.regSpot = spot; reg.regLast = now; } else { if (reg.nowait && reg.blockInterval > 0 && reg.regLast > 0) { // precise ms control const overflow = now - reg.regLast - reg.blockInterval + reg.regOverflow; let adjusted = reg.blockInterval - overflow; if (adjusted < 0) { adjusted = 0; } adjusted += this.timeblockCursor; while (adjusted >= this.timeblock.length) { adjusted -= this.timeblock.length; } const spot = this.timeblock[adjusted]; spot.push({reg}); reg.regSpot = spot; reg.regOverflow = overflow; } else { let adjusted = reg.blockInterval; adjusted += this.timeblockCursor; while (adjusted >= this.timeblock.length) { adjusted -= this.timeblock.length; } const spot = this.timeblock[reg.blockInterval]; spot.push({reg}); reg.regSpot = spot; reg.regLast = now; } } reg.regLast = now; reg.timerblock = this; reg.controller = this.controller; } else { this.getReplacement().setIntoBlock(reg); } } register(reg: TimerRegistrant) { if (!reg.regStart) { reg.regStart = Date.now(); } if (!this.controller.started) { this.controller.startBlocks(); } if (reg.immediate || reg.ms < 1) { this.controller.baseblock.setImmediate(reg); } else { if (!this.replacedBy) { this.setIntoBlock(reg); } else { this.getReplacement().setIntoBlock(reg); } } return reg; } start() { if (this.started) { return; } this.started = true; this.startTime = Date.now(); this.last = Date.now(); this.timeblock.length = this.maxBlocks; for (let i = 0; i < this.maxBlocks; ++i) { this.timeblock[i] = []; } this.checkingLogic = (immediatesOnly: false) => { const now = Date.now(); const repeatersList: TimerRegistrant[] = []; // Handle immediates for (let j = 0; j < this.immediates.length; ++j) { this.handleRegistration(this.immediates[j], now, repeatersList); } this.immediates = []; this.handlerRepeatersIfAny(repeatersList); if (immediatesOnly) { return; } // Handle timed blocked const delta = now - this.last; if (delta === 0) { return; } const rawDelta = this.notMsBlock ? delta / this.blockMs : delta; const blockCountToHandle = this.notMsBlock ? Math.floor(delta / this.blockMs) : delta; if (blockCountToHandle === 0) { return; } if (this.notMsBlock) { const timeOvercounted = Math.round((rawDelta - blockCountToHandle) * this.blockMs); this.last = now - timeOvercounted; } else { this.last = now; } if (this.timeblock.length === 0) { this.clearAllProcs(); return; } const blockCountToHandleSafe = Math.min(blockCountToHandle, this.timeblock.length); for (let j = 0; j < blockCountToHandleSafe; ++j) { let index = this.timeblockCursor + j; while(index >= this.timeblock.length) { index -= this.timeblock.length; } const ontheblock = this.timeblock[index]; for (const streg of ontheblock) { --this.pendingCount; this.handleRegistration(streg, now, repeatersList); } if (!this.replacedBy) { this.timeblock[index] = []; } else if (this.timeblock.length === 0) { this.clearAllProcs(); break; } } this.timeblockCursor += blockCountToHandleSafe; while (this.timeblockCursor >= this.timeblock.length) { this.timeblockCursor -= this.timeblock.length; } this.handlerRepeatersIfAny(repeatersList); this.totalHandledBlocksCount += blockCountToHandleSafe; }; for (let i = 0; i < this.procCount; ++i) { this.procs.push(setInterval(this.checkingLogic, this.procInterval)); } } private handlerRepeatersIfAny(repeatersList: TimerRegistrant[]) { if (repeatersList.length > 0) { for (const reg of repeatersList) { reg.timerblock.setIntoBlock(reg); } repeatersList.length = 0; } } private handleRegistration(streg: SharedTimerRegistration, now: number, repeatersList: TimerRegistrant[]) { const reg = streg.reg; if (!reg.active) { return; } reg.last = now; if (!reg.paused && reg.skipCount === 0) { reg.pre?.(reg); reg.callback(reg); const ibhKeys = reg.intervalBehavior ? Object.keys(reg.intervalBehavior) : null; if (ibhKeys && ibhKeys.length > 0) { for (const ibhKey of ibhKeys) { const bh = reg.intervalBehavior[ibhKey]; bh.callback?.(bh, reg); } } reg.post?.(reg); } if (reg.skipCount > 0) { --reg.skipCount; } if (reg.repeat) { if (!reg.waitBeforeRepeat) { repeatersList.push(reg); } else { ++this.pendingCount; reg.waitBeforeRepeat.then(async () => { if (reg.regSpot) { --this.pendingCount; } if (reg.active && reg.repeat) { reg.timerblock.setIntoBlock(reg); } }); reg.waitBeforeRepeat = null; } } } } export class SharedTimer extends Entity { name: string = ''; baseblock: SharedTimerBlock; timerblocks: SharedTimerBlock[] = []; started = false; checkerId: any; constructor(rubric: { name: string; timerblocks: Partial<SharedTimerBlock>[]}) { super('shared-timer'); this.name = rubric.name; for (const timerBlockConfig of rubric.timerblocks) { const timerblock = new SharedTimerBlock(timerBlockConfig); timerblock.controller = this; timerblock.lcManagedBy(this); this.timerblocks.push(timerblock); } this.baseblock = this.timerblocks[0]; this.addOnDestroy(() => { if (this.checkerId) { clearInterval(this.checkerId); } }); } startBlocks() { if (this.started) { return this; } this.started = true; for (const timerblock of this.timerblocks) { timerblock.start(); } return this; } endOnEmpty(checkingInterval: number = 200) { if (this.checkerId) { return this; } this.checkerId = setInterval(() => { let totalPending = 0; for (const timerblock of this.timerblocks) { totalPending += timerblock.pendingCount; } if (totalPending === 0) { this.destroy(); } }, checkingInterval); return this; } setPrecision(level: 'max' | 'ultra' | 'super' | 'faster' | 'default' | 'slower' | 'lazy' | 'superlazy' | 'ultralazy') { let procInterval = this.baseblock.procInterval; switch (level) { case 'max': procInterval = 0; break; case 'ultra': procInterval = 1; break; case 'super': procInterval = 2; break; case 'faster': procInterval = 5; break; case 'default': procInterval = 10; break; case 'slower': procInterval = 20; break; case 'lazy': procInterval = 33; break; case 'superlazy': procInterval = 50; break; case 'ultralazy': procInterval = 100; break; default: return; } if (this.baseblock.procInterval !== procInterval) { const oldBlock = this.baseblock; const newBlock = new SharedTimerBlock({ name: oldBlock.name, procInterval, procCount: oldBlock.procCount, blockMs: oldBlock.blockMs, maxBlocks: oldBlock.maxBlocks, }); newBlock.controller = this; newBlock.replacing = oldBlock; newBlock.start(); oldBlock.replacedBy = newBlock; this.baseblock = this.timerblocks[0] = newBlock; } return this; } } export const mainSharedTimer = new SharedTimer({ name: 'main', timerblocks: [ { name: 'up to 10s', procInterval: 1, blockMs: 1, maxBlocks: 10000 }, { name: 'up to 10m', procInterval: 200, blockMs: 100, maxBlocks: 6000 }, { name: 'up to 10h', procInterval: 1000, blockMs: 10000, maxBlocks: 3600 }, { name: 'up to 10d', procInterval: 1000, blockMs: 120000, maxBlocks: 7200 }, { name: 'up to 3mo', procInterval: 1000, blockMs: 10800000, maxBlocks: 720 }, ] }); mainSharedTimer.addOnDestroy(() => { ixConfig.sleepFunctions.useSharedTimer = false; }); export class Timer<T = any> extends Entity { static allActiveTimers: {[key: string]: Timer<any>} = {}; name = ''; options: TimerOptions; maxCount = 1; count = 0; paused = false; started = false; ended = false; finished = false; startTime: number = -1; endTime: number = -1; finishTime: number = -1; deltaEnd: number = -1; deltaFinish: number = -1; deltaEndFinish: number = -1; lastTick: number = -1; lastExec: number = -1; autoDestroy = false; result: T = null; error: Error = null; reg = new TimerRegistrant<Timer<T>>({ source: this }); regWatch: TimerRegistrant<Timer<T>>; private interval: number = 1000; private ontick: (e: TimerTickSnapshot<T>) => any; private onwatchtick: (e: Timer<T>) => any; private cond: ((timer: Timer) => boolean) | boolean = null; private endResolves: ((timer: Timer) => any)[] = []; private finishResolves: ((timer: Timer) => any)[] = []; private lingeringLogic: {[key: string]: Promise<any>} = {}; private endOnResult = false; private executing = false; private execProm: Promise<T>; constructor(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { super('timer'); if (!spec) { spec = {}; } if (typeof spec === 'number') { spec = { ms: spec }; } this.options = spec; this.ontick = ontick ? ontick : () => {}; this.interval = this.fromUnit(this.options); if (this.interval <= 0) { this.interval = 1000; } this.name = this.options.name ? this.options.name : `(timer-${this.ixId})`; this.addOnDestroy(() => { this.end(); }); if (this.options.count !== null && this.options.count !== undefined) { this.maxCount = this.options.count; } if (this.options.forever) { this.maxCount = 0; } if (!this.options.defer) { const startNow = this.options.immediately || this.options.startNow; this.nudge(startNow ? { immediately: true } : null); } } get ordinal() { return this.count + 1; } get nth() { return this.count + 1; } get elapsed() { return this.startTime < 0 ? 0 : Date.now() - this.startTime; } get now() { return Date.now(); } get endPromise() { return new Promise<Timer>(resolve => this.ended ? resolve(this) : this.endResolves.push(resolve)); } get finishPromise() { return new Promise<Timer>(resolve => this.finished ? resolve(this) : this.finishResolves.push(resolve)); } get beforetick$() { return this.ixRx<TimerTickSnapshot<T>>('beforetick').obs(); } get tick$() { return this.ixRx<TimerTickSnapshot<T>>('tick').obs(); } get aftertick$() { return this.ixRx<TimerTickSnapshot<T>>('aftertick').obs(); } get end$() { return this.ixRx<Timer<T>>('end').obs(); } get finish$() { return this.ixRx<Timer<T>>('finish').obs(); } asPromise() { return this.finishPromise as Promise<Timer>; } runOnce() { if (this.ended) { return null; } this.lastTick = Date.now(); if (this.startTime < 0) { this.started = true; this.startTime = Date.now(); } if (this.paused) { return null; } let isPromise = false; const e = this.getSnapshot(''); const preCondPassed = (this.cond !== false && this.cond === null || this.cond === true || (this.cond && this.cond(this))); if (preCondPassed && this.maxCount === 0 || this.count < this.maxCount) { e.context = 'beforetick'; this.ixRx<TimerTickSnapshot<T>>('beforetick').next(e); try { this.executing = true; this.lastExec = Date.now(); e.context = 'main'; const res = this.ontick(e); isPromise = (res && res.then); if (isPromise) { const iterId = this.count + ''; const iterProm = this.execProm = (res as Promise<T>); this.lingeringLogic[iterId] = iterProm; iterProm.then(r => { this.executing = false; this.result = e.result = r; this.error = e.error = null; if (this.lingeringLogic[iterId]) { delete this.lingeringLogic[iterId]; } e.context = 'aftertick-async'; this.ixRx<TimerTickSnapshot<T>>('aftertick').next(e); if (this.result && this.endOnResult) { this.end(); } this.detectStopCond(); }).catch(error => { this.executing = false; this.result = e.result = null; this.error = e.error = error; if (this.lingeringLogic[iterId]) { delete this.lingeringLogic[iterId]; } e.context = 'main-error-async'; this.ixError(error, null, e); e.context = 'aftertick-async'; this.ixRx<TimerTickSnapshot<T>>('aftertick').next(e); this.detectStopCond(); }); } else { this.executing = false; this.result = e.result = res; this.error = e.error = null; } } catch (error) { this.executing = false; this.error = e.error = error; } if (!e.error) { e.context = 'tick'; this.ixRx<TimerTickSnapshot<T>>('tick').next(e); } else { e.context = 'main-error'; this.ixError(e.error, null, e); } if (!isPromise) { e.context = 'aftertick'; this.ixRx<TimerTickSnapshot<T>>('aftertick').next(e); } this.count += 1; if (this.result && this.endOnResult) { this.end(); } } if (!isPromise) { this.detectStopCond(); } return isPromise ? this.execProm : null; } start() { if (this.started) { return this; } this.started = true; this.startTime = Date.now(); this.reg.nowait = !!this.options.nowait; this.reg.update({ immediate: !!(this.options?.immediately || this.options?.startNow), ms: this.interval, repeat: true, callback: reg => { const res = this.runOnce(); const needToWait = this.executing && !this.options.nowait; if (res && res.then && needToWait) { reg.waitBeforeRepeat = res; } } }); this.reg.registerOn(mainSharedTimer); return this; } immediate() { this.options.startNow = this.options.immediately = true; return this; } pause() { this.paused = true; return this; } resume() { this.paused = false; return this; } nudge(options?: { immediately?: boolean }, beforenudge?: () => any) { if (options?.immediately) { this.options.immediately = true; } if (!this.started) { promise(resolve => setTimeout(resolve, 0)).then(() => { if (beforenudge) { beforenudge(); } this.start(); }); } return this; } watch(ms?: number, onwatchtick?: (e: Timer<T>) => any) { if (!ms) { ms = 1000; } if (onwatchtick) { this.onwatchtick = onwatchtick; } this.startRegCondCheck(ms); this.regWatch.callback = () => { if (this.onwatchtick) { this.onwatchtick(this); } if (this.cond && typeof this.cond !== 'boolean') { const passed = this.cond(this); if (!passed) { this.end(); } } }; this.regWatch.registerOn(mainSharedTimer); this.regWatch.updateInterval(ms); return this; } attend(level: 'hyper' | 'super' | 'vigilant' | 'default' | 'lazy' | 'lazier' | 'superlazy' | 'ultralazy' = 'default', onwatchtick?: (e: Timer<T>) => any) { switch (level) { case 'hyper': return this.watch(1, onwatchtick); // 1ms case 'super': return this.watch(10, onwatchtick); // 10ms case 'vigilant': return this.watch(100, onwatchtick); // 100 ms case 'default': return this.watch(null, onwatchtick); // 1000 ms case 'lazy': return this.watch(1000, onwatchtick); // 1 sec case 'lazier': return this.watch(10001, onwatchtick); // 10 sec case 'superlazy': return this.watch(60000, onwatchtick); // 1 min case 'ultralazy': return this.watch(600000, onwatchtick); // 10 min default: return this; } } stop() { return this.end(); } end() { if (this.ended) { return this; } if (this.startTime < 0) { this.started = true; this.startTime = Date.now(); } this.ended = true; this.endTime = Date.now(); this.deltaEnd = this.endTime - this.startTime; if (this.reg) { this.reg.invalidate(); } if (this.regWatch) { this.regWatch.invalidate(); } this.ixRx<Timer<T>>('end').next(this); for (const resolve of this.endResolves) { try { resolve(this); } catch (error) { const e = this.getSnapshot('end-error'); e.error = error; this.ixError(error, 1, e); } } const lingerIterationsKeys = Object.keys(this.lingeringLogic); const finishingNow = (this.options.endSharp || lingerIterationsKeys.length === 0); if (finishingNow) { this.finishingWrapUp(); } else { const lingeringProms = lingerIterationsKeys.map(key => this.lingeringLogic[key]); PromUtil.allSettled(lingeringProms).then(() => { this.finishingWrapUp(); }); } return this; } /** * ```txt * [-t-][main][-t-][main][-t-]... * ```` */ forever() { this.maxCount = 0; this.nudge(); return this; } /** * ```txt * [main][-t-][main][-t-]... * ```` */ nowAndForever() { this.maxCount = 0; this.nudge({ immediately: true }); return this; } /** * ```txt * [-t-][main][-t-][main][-t-]...[main] * |--------- n count -----------| * ```` * @param count how many time should the function execute */ countUp(count?: number) { if (count <= 0 || !Number.isFinite(count)) { throw new Error(`'count' argument of value '${count} is not valid'`); } this.maxCount = count; this.nudge(); return this; } /** * ```txt * [main][-t-][main][-t-]...[main] * |--------- n count -----------| * ```` * @param count how many time should the function execute */ countUpNow(count?: number) { if (count <= 0 || !Number.isFinite(count)) { throw new Error(`'count' argument of value '${count} is not valid'`); } this.maxCount = count; this.nudge({ immediately: true }); return this; } /** * ```txt * [-t-][first][-t-][repeat1][-t-][repeat2]...[repeat-n] * |--------- repeat count -----------| * ```` * @param count how many time should the function should *repeat* (first execution not counting) */ repeatFor(count?: number) { if (count <= 0 || !Number.isFinite(count)) { throw new Error(`'count' argument of value '${count} is not valid'`); } return this.countUp(count + 1); } /** * ```txt * [first][-t-][repeat1][-t-][repeat2]...[repeat-n] * |--------- repeat count -----------| * ```` * @param count how many time should the function should *repeat* (first execution not counting */ nowAndRepeatFor(count?: number) { if (count <= 0 || !Number.isFinite(count)) { throw new Error(`'count' argument of value '${count} is not valid'`); } return this.countUpNow(count + 1); } until(cond: ((timer: Timer) => boolean) | boolean) { this.cond = typeof cond === 'boolean' ? !cond : () => !cond(this); this.maxCount = 0; this.nudge(); return this; } while(cond: ((timer: Timer) => boolean) | boolean) { this.maxCount = 0; this.cond = cond; this.nudge(); return this; } nowAndWhile(cond: ((timer: Timer) => boolean) | boolean) { this.maxCount = 0; this.cond = cond; this.nudge({ immediately: true }); return this; } untilTime(ts: number) { this.while(() => Date.now() <= ts); return this; } untilTimeNow(ts: number) { this.nowAndWhile(() => Date.now() <= ts); return this; } forDuration(t: TimerUnitTime) { const ms = this.fromUnit(t); if (ms < 0) { throw new Error(`Given profile has no time units in [ms,s,m,h,d,w,mo,yr]`); } const startTime = Date.now(); this.while(() => Date.now() - startTime <= ms); return this; } forDurationNow(t: TimerUnitTime) { const ms = this.fromUnit(t); if (ms < 0) { throw new Error(`Given profile has no time units in [ms,s,m,h,d,w,mo,yr]`); } const startTime = Date.now(); this.nowAndWhile(() => Date.now() - startTime <= ms); return this; } untilResult(type: BackOffType = { backoff: 'default' }, options?: RetryUntilResultOptions) { this.endOnResult = true; this.retryArgsPrep(type, 'untilResult', options); return this.while(() => this.result === null || this.result === undefined); } untilResultNow(type: BackOffType = { backoff: 'default' }, options?: RetryUntilResultOptions) { this.endOnResult = true; this.retryArgsPrep(type, 'untilResult', options); return this.nowAndWhile(() => this.result === null || this.result === undefined); } backOff(type: 'expo' | 'linear', max = 1200, value = 1, start = this.interval / 1000) { this.retryArgsPrep({backoff: type}, 'backOff', {backoff: {type, max, value, start}}); return this.forever(); } backOffNow(type: 'expo' | 'linear', max = 1200, value = 1, start = this.interval / 1000) { this.retryArgsPrep({backoff: type}, 'backOff', {backoff: {type, max, value, start}}); return this.nowAndForever(); } andOutputTime(nameProfile?: string) { this.finishPromise.then(() => { if (!nameProfile) { nameProfile = this.name; } // tslint:disable-next-line: no-console console['log']( `${nameProfile} took ${this.deltaFinish} ms.\n` + ` └─ start: ${this.startTime}, end: ${this.endTime}, finish: ${this.finishTime}\n` + (this.result ? ` └─ result: ${this.result}` : ``) ); }); return this; } andDestroy() { this.autoDestroy = true; if (this.endTime > 0) { this.destroy(); } return this; } newBackOffOptions(type: 'expo' | 'linear', max = 1200, value = 1, start = this.interval / 1000) { let profile: RetryUntilResultOptions; if (type === 'expo') { profile = { backoff: { type, start, value, max } }; } else if (type === 'linear') { profile = { backoff: { type, start, value, max } }; } return profile; } updateInterval(newIntervalMs: number) { this.reg.updateInterval(newIntervalMs); } newBackOffIntervalBehavior(options: RetryUntilResultOptions): TimerIntervalBehavior<Timer<T>> { return { data: {}, begun: false, callback: bh => { const boConf = options.backoff; if (boConf) { if (!bh.begun) { this.interval = boConf.start * 1000; bh.begun = true; } else { const maxMs = boConf.max * 1000; if (boConf.type === 'expo') { if (this.interval < maxMs) { this.interval *= (Math.E * boConf.value); if (this.interval > maxMs) { this.interval = maxMs; } this.updateInterval(this.interval); } } else if (boConf.type === 'linear') { if (this.interval < maxMs) { this.interval += boConf.value * 1000; if (this.interval > maxMs) { this.interval = maxMs; } this.updateInterval(this.interval); } } } } } }; } private startRegCondCheck(ms: number) { if (!this.regWatch) { this.regWatch = new TimerRegistrant<Timer<T>>({ source: this, ms, repeat: true, immediate: true }); } } private getSnapshot(context: string) { const now = Date.now(); const e: TimerTickSnapshot<T> = { now, result: this.result, context, maxCount: this.maxCount, count: this.count, nth: this.count + 1, ordinal: this.count + 1, elapsed: now - this.startTime, timer: this, }; return e; } private detectStopCond() { const shouldStop = (this.cond === false || (this.cond && typeof this.cond !== 'boolean' && !this.cond(this))); if (shouldStop || (this.maxCount !== 0 && this.count === this.maxCount)) { this.end(); } } private fromUnit(units: TimerUnitTime) { let ms = -1; for (const unit of Object.keys(units)) { if (unit.length > 2 || unit === 't') { continue; } // units are one or two chars const n = units[unit]; if (!n || !Number.isFinite(n)) { continue; } if (unit === 'ms') { ms = n; break; } if (unit === 's') { ms = n * 1000; break; } if (unit === 'm') { ms = n * 60000; break; } if (unit === 'h') { ms = n * 3600000; break; } if (unit === 'd') { ms = n * 86400000; break; } if (unit === 'w') { ms = n * 604800000; break; } if (unit === 'mo') { ms = n * 2592000000; break; } if (unit === 'yr') { ms = n * 31536000000; break; } } return ms; } private retryArgsPrep(type: BackOffType = { backoff: 'default' }, context = 'default', options?: RetryUntilResultOptions) { const bhKey = `${this.ixId}-${context}`; if (type.backoff === 'default') { if (this.reg.intervalBehavior[bhKey]) { delete this.reg.intervalBehavior[bhKey]; } } else { const retryOptions = options ? options : this.newBackOffOptions(type.backoff); const bh = this.newBackOffIntervalBehavior(retryOptions); this.reg.intervalBehavior[bhKey] = bh; } } private finishingWrapUp() { if (this.finished) { return this; } this.finished = true; this.finishTime = Date.now(); this.deltaFinish = this.finishTime - this.startTime; this.deltaEndFinish = this.finishTime - this.endTime; this.ixRx<Timer<T>>('finish').next(this); for (const resolve of this.finishResolves) { try { resolve(this); } catch (error) { const e = this.getSnapshot('finish-error'); e.error = error; this.ixError(error, 1, e); } } if (this.autoDestroy) { this.destroy(); } } } export class TimerRegistrant<T = any> { source?: T; name?: string = ''; controller?: SharedTimer; timerblock?: SharedTimerBlock; data?: any; ms?: number = -1; msBefore?: number = -2; immediate?: boolean = false; blockInterval?: number = -1; repeat?: boolean = false; waitBeforeRepeat?: Promise<any>; paused?: boolean = false; skipCount?: number = 0; active?: boolean = true; last?: number = 0; nowait?: boolean = false; regStart?: number = 0; regLast?: number = 0; regOverflow?: number = 0; regSpot?: SharedTimerRegistration[]; pre?: (reg: TimerRegistrant) => any; callback?: (reg: TimerRegistrant) => any; intervalBehavior?: {[key: string]: TimerIntervalBehavior} = {}; post?: (reg: TimerRegistrant) => any; constructor(init?: Partial<TimerRegistrant<T>>) { if (init) { Object.assign(this, init); } if (this.ms < 0) { this.ms = 0; } this.ms = Math.ceil(this.ms); } invalidate() { this.active = false; if (this.regSpot) { let i = 0; for (const regInfo of this.regSpot) { if (regInfo.reg === this) { --this.timerblock.pendingCount; this.regSpot.splice(i, 1); break; } ++i; } } this.regSpot = null; } update(updater?: Partial<TimerRegistrant<T>>) { if (updater) { Object.assign(this, updater); }} setImmediate() { this.controller?.baseblock?.setImmediate(this); return this; } registerOn(t?: SharedTimer) { this.updatePlacement(t)?.register(this); return this; } updateInterval(newMs: number) { this.ms = Math.ceil(newMs); this.updatePlacement(); } updatePlacement(t?: SharedTimer) { if (!t) { t = this.controller; if (!t) { return null; } } if (t !== this.controller) { this.msBefore = -1; this.timerblock = null; } if (this.msBefore === this.ms) { return null; } let matched: SharedTimerBlock = null; for (const timerblock of t.timerblocks) { if (this.ms <= timerblock.maxSupportedMs) { matched = timerblock; break; } } this.controller = t; this.timerblock = matched; this.msBefore = this.ms; this.blockInterval = this.timerblock.blockMs !== 1 ? Math.ceil(this.ms / this.timerblock.blockMs) : Math.ceil(this.ms); return this.timerblock; } } // export function immediate<T = any>(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { // return new Timer(spec, ontick).immediate(); // } // export function timer<T = any>(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { // return new Timer(spec, ontick); // } export function timerInterval<T = any>(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { return new Timer(spec, ontick).forever(); } // export function intervalNow<T = any>(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { // return new Timer(spec, ontick).immediate().forever(); // } // export function retry<T = any>(timeoutInSeconds: number, func: (ti?: TimerTickSnapshot<T>) => Promise<T>) { // return new Promise<T>(async resolve => { // const timeoutInMs = timeoutInSeconds * 1000; // const ti = new Timer<T>(1000, async t => { // try { return await func(t); } catch (e) { errorCast(e); } // }).watch(1000, () => { if (ti.elapsed > timeoutInMs) { ti.end(); } }); // // tslint:disable-next-line: deprecation // ti.finish$.subscribe(() => { resolve(ti.result); }); // ti.untilResultNow({backoff: 'expo'}); // }); // } // export function sleep(s: number, afterSleeping?: () => any) { // if (!s || s < 0) { s = 0; } // return new Promise<void>(resolve => { // if (ixConfig.sleepFunctions.useSharedTimer) { // new TimerRegistrant({ ms: s * 1000, callback: () => { // if (afterSleeping) { afterSleeping(); } // resolve(); // }}).registerOn(mainSharedTimer); // } else { // setTimeout(resolve, s * 1000); // } // }); // } // export function sleepms(ms: number, afterSleeping?: () => any) { // if (!ms || ms < 0) { ms = 0; } // return new Promise<void>(resolve => { // if (false && ixConfig.sleepFunctions.useSharedTimer) { // new TimerRegistrant({ ms, callback: () => { // if (afterSleeping) { afterSleeping(); } // resolve(); // }}).registerOn(mainSharedTimer); // } else { // setTimeout(resolve, ms); // } // }); // } // export function timeout<T = any>(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { // return new Timer(spec, ontick); // } // export function repeater<T = any>(spec?: number | TimerOptions, ontick?: (e: TimerTickSnapshot<T>) => any) { // const ti = new Timer(spec, ontick); // ti.forever(); // return ti; // }