/* This file is part of GNU Taler (C) 2017-2019 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ /** * Helpers for relative and absolute time. */ /** * Imports. */ import { Codec, Context, renderContext } from "./codec.js"; declare const flavor_AbsoluteTime: unique symbol; declare const flavor_TalerProtocolTimestamp: unique symbol; declare const flavor_TalerPreciseTimestamp: unique symbol; const opaque_AbsoluteTime: unique symbol = Symbol("opaque_AbsoluteTime"); // FIXME: Make this opaque! export interface AbsoluteTime { /** * Timestamp in milliseconds. */ readonly t_ms: number | "never"; readonly _flavor?: typeof flavor_AbsoluteTime; // Make the type opaque, we only want our constructors // to able to create an AbsoluteTime value. [opaque_AbsoluteTime]: true; } export interface TalerProtocolTimestamp { /** * Seconds (as integer) since epoch. */ readonly t_s: number | "never"; readonly _flavor?: typeof flavor_TalerProtocolTimestamp; } /** * Precise timestamp, typically used in the wallet-core * API but not in other Taler APIs so far. */ export interface TalerPreciseTimestamp { /** * Seconds (as integer) since epoch. */ readonly t_s: number | "never"; /** * Optional microsecond offset (non-negative integer). */ readonly off_us?: number; readonly _flavor?: typeof flavor_TalerPreciseTimestamp; } export namespace TalerPreciseTimestamp { export function now(): TalerPreciseTimestamp { const absNow = AbsoluteTime.now(); return AbsoluteTime.toPreciseTimestamp(absNow); } export function round(t: TalerPreciseTimestamp): TalerProtocolTimestamp { return { t_s: t.t_s, }; } export function fromSeconds(s: number): TalerPreciseTimestamp { return { t_s: Math.floor(s), off_us: Math.floor((s - Math.floor(s)) / 1000 / 1000), }; } export function fromMilliseconds(ms: number): TalerPreciseTimestamp { return { t_s: Math.floor(ms / 1000), off_us: Math.floor((ms - Math.floor(ms / 1000) * 1000) * 1000), }; } } export namespace TalerProtocolTimestamp { export function now(): TalerProtocolTimestamp { return AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()); } export function zero(): TalerProtocolTimestamp { return { t_s: 0, }; } export function never(): TalerProtocolTimestamp { return { t_s: "never", }; } export function isNever(t: TalerProtocolTimestamp): boolean { return t.t_s === "never"; } export function fromSeconds(s: number): TalerProtocolTimestamp { return { t_s: s, }; } export function min( t1: TalerProtocolTimestamp, t2: TalerProtocolTimestamp, ): TalerProtocolTimestamp { if (t1.t_s === "never") { return { t_s: t2.t_s }; } if (t2.t_s === "never") { return { t_s: t1.t_s }; } return { t_s: Math.min(t1.t_s, t2.t_s) }; } export function max( t1: TalerProtocolTimestamp, t2: TalerProtocolTimestamp, ): TalerProtocolTimestamp { if (t1.t_s === "never" || t2.t_s === "never") { return { t_s: "never" }; } return { t_s: Math.max(t1.t_s, t2.t_s) }; } } export interface Duration { /** * Duration in milliseconds. */ readonly d_ms: number | "forever"; } export interface TalerProtocolDuration { readonly d_us: number | "forever"; } /** * Timeshift in milliseconds. */ let timeshift = 0; /** * Set timetravel offset in milliseconds. * * Use carefully and only for testing. */ export function setDangerousTimetravel(dt: number): void { timeshift = dt; } export namespace Duration { export function toMilliseconds(d: Duration): number { if (d.d_ms === "forever") { return Number.MAX_VALUE; } return d.d_ms; } export function getRemaining( deadline: AbsoluteTime, now = AbsoluteTime.now(), ): Duration { if (deadline.t_ms === "never") { return { d_ms: "forever" }; } if (now.t_ms === "never") { throw Error("invalid argument for 'now'"); } if (deadline.t_ms < now.t_ms) { return { d_ms: 0 }; } return { d_ms: deadline.t_ms - now.t_ms }; } export function fromPrettyString(s: string): Duration { let dMs = 0; let currentNum = ""; let parsingNum = true; for (let i = 0; i < s.length; i++) { const cc = s.charCodeAt(i); if (cc >= "0".charCodeAt(0) && cc <= "9".charCodeAt(0)) { if (!parsingNum) { throw Error("invalid duration, unexpected number"); } currentNum += s[i]; continue; } if (s[i] == " ") { if (currentNum != "") { parsingNum = false; } continue; } if (currentNum == "") { throw Error("invalid duration, missing number"); } if (s[i] === "s") { dMs += 1000 * Number.parseInt(currentNum, 10); } else if (s[i] === "m") { dMs += 60 * 1000 * Number.parseInt(currentNum, 10); } else if (s[i] === "h") { dMs += 60 * 60 * 1000 * Number.parseInt(currentNum, 10); } else if (s[i] === "d") { dMs += 24 * 60 * 60 * 1000 * Number.parseInt(currentNum, 10); } else { throw Error("invalid duration, unsupported unit"); } currentNum = ""; parsingNum = true; } return { d_ms: dMs, }; } /** * Compare two durations. Returns 0 when equal, -1 when a < b * and +1 when a > b. */ export function cmp(d1: Duration, d2: Duration): 1 | 0 | -1 { if (d1.d_ms === "forever") { if (d2.d_ms === "forever") { return 0; } return 1; } if (d2.d_ms === "forever") { return -1; } if (d1.d_ms == d2.d_ms) { return 0; } if (d1.d_ms > d2.d_ms) { return 1; } return -1; } export function max(d1: Duration, d2: Duration): Duration { return durationMax(d1, d2); } export function min(d1: Duration, d2: Duration): Duration { return durationMin(d1, d2); } export function multiply(d1: Duration, n: number): Duration { return durationMul(d1, n); } export function toIntegerYears(d: Duration): number { if (typeof d.d_ms !== "number") { throw Error("infinite duration"); } return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365); } export function fromSpec(spec: { seconds?: number; minutes?: number; hours?: number; days?: number; months?: number; years?: number; }): Duration { let d_ms = 0; d_ms += (spec.seconds ?? 0) * SECONDS; d_ms += (spec.minutes ?? 0) * MINUTES; d_ms += (spec.hours ?? 0) * HOURS; d_ms += (spec.days ?? 0) * DAYS; d_ms += (spec.months ?? 0) * MONTHS; d_ms += (spec.years ?? 0) * YEARS; return { d_ms }; } export function getForever(): Duration { return { d_ms: "forever" }; } export function getZero(): Duration { return { d_ms: 0 }; } export function fromTalerProtocolDuration( d: TalerProtocolDuration, ): Duration { if (d.d_us === "forever") { return { d_ms: "forever", }; } return { d_ms: Math.floor(d.d_us / 1000), }; } export function toTalerProtocolDuration(d: Duration): TalerProtocolDuration { if (d.d_ms === "forever") { return { d_us: "forever", }; } return { d_us: d.d_ms * 1000, }; } export function fromMilliseconds(ms: number): Duration { return { d_ms: ms, }; } export function clamp(args: { lower: Duration; upper: Duration; value: Duration; }): Duration { return durationMax(durationMin(args.value, args.upper), args.lower); } } export namespace AbsoluteTime { export function getStampMsNow(): number { return new Date().getTime(); } export function getStampMsNever(): number { return Number.MAX_SAFE_INTEGER; } export function now(): AbsoluteTime { return { t_ms: new Date().getTime() + timeshift, [opaque_AbsoluteTime]: true, }; } export function never(): AbsoluteTime { return { t_ms: "never", [opaque_AbsoluteTime]: true, }; } export function fromMilliseconds(ms: number): AbsoluteTime { return { t_ms: ms, [opaque_AbsoluteTime]: true, }; } export function cmp(t1: AbsoluteTime, t2: AbsoluteTime): number { if (t1.t_ms === "never") { if (t2.t_ms === "never") { return 0; } return 1; } if (t2.t_ms === "never") { return -1; } if (t1.t_ms == t2.t_ms) { return 0; } if (t1.t_ms > t2.t_ms) { return 1; } return -1; } export function min(t1: AbsoluteTime, t2: AbsoluteTime): AbsoluteTime { if (t1.t_ms === "never") { return { t_ms: t2.t_ms, [opaque_AbsoluteTime]: true }; } if (t2.t_ms === "never") { return { t_ms: t2.t_ms, [opaque_AbsoluteTime]: true }; } return { t_ms: Math.min(t1.t_ms, t2.t_ms), [opaque_AbsoluteTime]: true }; } export function max(t1: AbsoluteTime, t2: AbsoluteTime): AbsoluteTime { if (t1.t_ms === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } if (t2.t_ms === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } return { t_ms: Math.max(t1.t_ms, t2.t_ms), [opaque_AbsoluteTime]: true }; } export function difference(t1: AbsoluteTime, t2: AbsoluteTime): Duration { if (t1.t_ms === "never") { return { d_ms: "forever" }; } if (t2.t_ms === "never") { return { d_ms: "forever" }; } return { d_ms: Math.abs(t1.t_ms - t2.t_ms) }; } export function isExpired(t: AbsoluteTime) { return cmp(t, now()) <= 0; } export function isNever(t: AbsoluteTime): boolean { return t.t_ms === "never"; } export function fromProtocolTimestamp( t: TalerProtocolTimestamp, ): AbsoluteTime { if (t.t_s === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } return { t_ms: t.t_s * 1000, [opaque_AbsoluteTime]: true, }; } export function fromStampMs(stampMs: number): AbsoluteTime { return { t_ms: stampMs, [opaque_AbsoluteTime]: true, }; } export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime { if (t.t_s === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } const offsetUs = t.off_us ?? 0; return { t_ms: t.t_s * 1000 + Math.floor(offsetUs / 1000), [opaque_AbsoluteTime]: true, }; } export function toStampMs(at: AbsoluteTime): number { if (at.t_ms === "never") { return Number.MAX_SAFE_INTEGER; } return at.t_ms; } export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp { if (at.t_ms == "never") { return { t_s: "never", }; } const t_s = Math.floor(at.t_ms / 1000); const off_us = Math.floor(1000 * (at.t_ms - t_s * 1000)); return { t_s, off_us, }; } export function toProtocolTimestamp( at: AbsoluteTime, ): TalerProtocolTimestamp { if (at.t_ms === "never") { return { t_s: "never" }; } return { t_s: Math.floor(at.t_ms / 1000), }; } export function isBetween( t: AbsoluteTime, start: AbsoluteTime, end: AbsoluteTime, ): boolean { if (cmp(t, start) < 0) { return false; } if (cmp(t, end) > 0) { return false; } return true; } export function toIsoString(t: AbsoluteTime): string { if (t.t_ms === "never") { return ""; } else { return new Date(t.t_ms).toISOString(); } } export function addDuration(t1: AbsoluteTime, d: Duration): AbsoluteTime { if (t1.t_ms === "never" || d.d_ms === "forever") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } return { t_ms: t1.t_ms + d.d_ms, [opaque_AbsoluteTime]: true }; } /** * Get the remaining duration until {@param t1}. * * If {@param t1} already happened, the remaining duration * is zero. */ export function remaining(t1: AbsoluteTime): Duration { if (t1.t_ms === "never") { return Duration.getForever(); } const stampNow = now(); if (stampNow.t_ms === "never") { throw Error("invariant violated"); } return Duration.fromMilliseconds(Math.max(0, t1.t_ms - stampNow.t_ms)); } export function subtractDuraction( t1: AbsoluteTime, d: Duration, ): AbsoluteTime { if (t1.t_ms === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } if (d.d_ms === "forever") { return { t_ms: 0, [opaque_AbsoluteTime]: true }; } return { t_ms: Math.max(0, t1.t_ms - d.d_ms), [opaque_AbsoluteTime]: true }; } export function stringify(t: AbsoluteTime): string { if (t.t_ms === "never") { return "never"; } return new Date(t.t_ms).toISOString(); } } const SECONDS = 1000; const MINUTES = SECONDS * 60; const HOURS = MINUTES * 60; const DAYS = HOURS * 24; const MONTHS = DAYS * 30; const YEARS = DAYS * 365; export function durationMin(d1: Duration, d2: Duration): Duration { if (d1.d_ms === "forever") { return { d_ms: d2.d_ms }; } if (d2.d_ms === "forever") { return { d_ms: d1.d_ms }; } return { d_ms: Math.min(d1.d_ms, d2.d_ms) }; } export function durationMax(d1: Duration, d2: Duration): Duration { if (d1.d_ms === "forever") { return { d_ms: "forever" }; } if (d2.d_ms === "forever") { return { d_ms: "forever" }; } return { d_ms: Math.max(d1.d_ms, d2.d_ms) }; } export function durationMul(d: Duration, n: number): Duration { if (d.d_ms === "forever") { return { d_ms: "forever" }; } return { d_ms: Math.round(d.d_ms * n) }; } export function durationAdd(d1: Duration, d2: Duration): Duration { if (d1.d_ms === "forever" || d2.d_ms === "forever") { return { d_ms: "forever" }; } return { d_ms: d1.d_ms + d2.d_ms }; } export const codecForAbsoluteTime: Codec = { decode(x: any, c?: Context): AbsoluteTime { const t_ms = x.t_ms; if (typeof t_ms === "string") { if (t_ms === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } } else if (typeof t_ms === "number") { return { t_ms, [opaque_AbsoluteTime]: true }; } throw Error(`expected timestamp at ${renderContext(c)}`); }, }; export const codecForTimestamp: Codec = { decode(x: any, c?: Context): TalerProtocolTimestamp { // Compatibility, should be removed soon. const t_ms = x.t_ms; if (typeof t_ms === "string") { if (t_ms === "never") { return { t_s: "never" }; } } else if (typeof t_ms === "number") { return { t_s: Math.floor(t_ms / 1000) }; } const t_s = x.t_s; if (typeof t_s === "string") { if (t_s === "never") { return { t_s: "never" }; } throw Error(`expected timestamp at ${renderContext(c)}`); } if (typeof t_s === "number") { return { t_s }; } throw Error(`expected protocol timestamp at ${renderContext(c)}`); }, }; export const codecForPreciseTimestamp: Codec = { decode(x: any, c?: Context): TalerPreciseTimestamp { const t_ms = x.t_ms; if (typeof t_ms === "string") { if (t_ms === "never") { return { t_s: "never" }; } } else if (typeof t_ms === "number") { return { t_s: Math.floor(t_ms / 1000) }; } throw Error(`expected precise timestamp at ${renderContext(c)}`); }, }; export const codecForDuration: Codec = { decode(x: any, c?: Context): TalerProtocolDuration { const d_us = x.d_us; if (typeof d_us === "string") { if (d_us === "forever") { return { d_us: "forever" }; } throw Error(`expected duration at ${renderContext(c)}`); } if (typeof d_us === "number") { return { d_us }; } throw Error(`expected duration at ${renderContext(c)}`); }, };