diff options
Diffstat (limited to 'packages/taler-util/src/time.ts')
-rw-r--r-- | packages/taler-util/src/time.ts | 716 |
1 files changed, 561 insertions, 155 deletions
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index c0858ada6..95b4911a0 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -21,13 +21,130 @@ /** * Imports. */ -import { Codec, renderContext, Context } from "./codec.js"; +import { Codec, Context, renderContext } from "./codec.js"; -export class Timestamp { +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 { @@ -37,56 +154,411 @@ export interface Duration { 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 function getTimestampNow(): Timestamp { - return { - t_ms: new Date().getTime() + timeshift, - }; -} +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 isTimestampExpired(t: Timestamp) { - return timestampCmp(t, getTimestampNow()) <= 0; -} + 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; + } -export function getDurationRemaining( - deadline: Timestamp, - now = getTimestampNow(), -): Duration { - if (deadline.t_ms === "never") { - return { d_ms: "forever" }; + 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); } - if (now.t_ms === "never") { - throw Error("invalid argument for 'now'"); + + 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" }; } - if (deadline.t_ms < now.t_ms) { + + export function getZero(): Duration { return { d_ms: 0 }; } - return { d_ms: deadline.t_ms - now.t_ms }; -} -export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp { - if (t1.t_ms === "never") { - return { t_ms: t2.t_ms }; + 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, + }; } - if (t2.t_ms === "never") { - return { t_ms: t2.t_ms }; + + 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); } - return { t_ms: Math.min(t1.t_ms, t2.t_ms) }; } -export function timestampMax(t1: Timestamp, t2: Timestamp): Timestamp { - if (t1.t_ms === "never") { - return { t_ms: "never" }; +export namespace AbsoluteTime { + export function getStampMsNow(): number { + return new Date().getTime(); + } + + export function getStampMsNever(): number { + return Number.MAX_SAFE_INTEGER; } - if (t2.t_ms === "never") { - return { t_ms: "never" }; + + 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 "<never>"; + } 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(); } - return { t_ms: Math.max(t1.t_ms, t2.t_ms) }; } const SECONDS = 1000; @@ -96,43 +568,12 @@ const DAYS = HOURS * 24; const MONTHS = DAYS * 30; const YEARS = DAYS * 365; -export function durationFromSpec(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 }; -} - -/** - * Truncate a timestamp so that that it represents a multiple - * of seconds. The timestamp is always rounded down. - */ -export function timestampTruncateToSecond(t1: Timestamp): Timestamp { - if (t1.t_ms === "never") { - return { t_ms: "never" }; - } - return { - t_ms: Math.floor(t1.t_ms / 1000) * 1000, - }; -} - 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: d2.d_ms }; + return { d_ms: d1.d_ms }; } return { d_ms: Math.min(d1.d_ms, d2.d_ms) }; } @@ -161,111 +602,76 @@ export function durationAdd(d1: Duration, d2: Duration): Duration { return { d_ms: d1.d_ms + d2.d_ms }; } -export function timestampCmp(t1: Timestamp, t2: Timestamp): number { - if (t1.t_ms === "never") { - if (t2.t_ms === "never") { - return 0; +export const codecForAbsoluteTime: Codec<AbsoluteTime> = { + decode(x: any, c?: Context): AbsoluteTime { + if (x === undefined) { + throw Error(`got undefined and expected absolute time at ${renderContext(c)}`); } - 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 timestampAddDuration(t1: Timestamp, d: Duration): Timestamp { - if (t1.t_ms === "never" || d.d_ms === "forever") { - return { t_ms: "never" }; - } - return { t_ms: t1.t_ms + d.d_ms }; -} - -export function timestampSubtractDuraction( - t1: Timestamp, - d: Duration, -): Timestamp { - if (t1.t_ms === "never") { - return { t_ms: "never" }; - } - if (d.d_ms === "forever") { - return { t_ms: 0 }; - } - return { t_ms: Math.max(0, t1.t_ms - d.d_ms) }; -} - -export function stringifyTimestamp(t: Timestamp): string { - if (t.t_ms === "never") { - return "never"; - } - return new Date(t.t_ms).toISOString(); -} - -export function timestampDifference(t1: Timestamp, t2: Timestamp): 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 timestampToIsoString(t: Timestamp): string { - if (t.t_ms === "never") { - return "<never>"; - } else { - return new Date(t.t_ms).toISOString(); - } -} - -export function timestampIsBetween( - t: Timestamp, - start: Timestamp, - end: Timestamp, -): boolean { - if (timestampCmp(t, start) < 0) { - return false; - } - if (timestampCmp(t, end) > 0) { - return false; - } - return true; -} + 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<Timestamp> = { - decode(x: any, c?: Context): Timestamp { +export const codecForTimestamp: Codec<TalerProtocolTimestamp> = { + decode(x: any, c?: Context): TalerProtocolTimestamp { + // Compatibility, should be removed soon. + if (x === undefined) { + throw Error(`got undefined and expected timestamp at ${renderContext(c)}`); + } const t_ms = x.t_ms; if (typeof t_ms === "string") { if (t_ms === "never") { - return { 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_ms === "number") { - return { t_ms }; + if (typeof t_s === "number") { + return { t_s }; } - throw Error(`expected timestamp at ${renderContext(c)}`); + throw Error(`expected protocol timestamp at ${renderContext(c)}`); + }, +}; + +export const codecForPreciseTimestamp: Codec<TalerPreciseTimestamp> = { + 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<Duration> = { - decode(x: any, c?: Context): Duration { - const d_ms = x.d_ms; - if (typeof d_ms === "string") { - if (d_ms === "forever") { - return { d_ms: "forever" }; +export const codecForDuration: Codec<TalerProtocolDuration> = { + 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_ms === "number") { - return { d_ms }; + if (typeof d_us === "number") { + return { d_us }; } throw Error(`expected duration at ${renderContext(c)}`); }, |