diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-12-19 20:42:49 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-12-19 20:42:49 +0100 |
commit | 0c9358c1b2bd80e25940022e86bd8daef8184ad7 (patch) | |
tree | a8c8ca0134bd886d8151633aff4c85e9513ad32c /src/util | |
parent | 49e3b3e5b9bbf1ce356ef68f301d50c689ceecb9 (diff) | |
download | wallet-core-0c9358c1b2bd80e25940022e86bd8daef8184ad7.tar.xz |
new date format, replace checkable annotations with codecs
Diffstat (limited to 'src/util')
-rw-r--r-- | src/util/RequestThrottler.ts | 14 | ||||
-rw-r--r-- | src/util/amounts.ts | 34 | ||||
-rw-r--r-- | src/util/checkable.ts | 417 | ||||
-rw-r--r-- | src/util/codec.ts | 54 | ||||
-rw-r--r-- | src/util/helpers.ts | 71 | ||||
-rw-r--r-- | src/util/time.ts | 165 | ||||
-rw-r--r-- | src/util/timer.ts | 22 |
7 files changed, 257 insertions, 520 deletions
diff --git a/src/util/RequestThrottler.ts b/src/util/RequestThrottler.ts index 01695ec37..0566306de 100644 --- a/src/util/RequestThrottler.ts +++ b/src/util/RequestThrottler.ts @@ -21,7 +21,7 @@ /** * Imports. */ -import { getTimestampNow, Timestamp } from "../types/walletTypes"; +import { getTimestampNow, Timestamp, timestampSubtractDuraction, timestampDifference } from "../util/time"; /** * Maximum request per second, per origin. @@ -50,10 +50,14 @@ class OriginState { private refill(): void { const now = getTimestampNow(); - const d = now.t_ms - this.lastUpdate.t_ms; - this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d / 1000)); - this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d / 1000 * 60)); - this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d / 1000 * 60 * 60)); + const d = timestampDifference(now, this.lastUpdate); + if (d.d_ms === "forever") { + throw Error("assertion failed") + } + const d_s = d.d_ms / 1000; + this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d_s / 1000)); + this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d_s / 1000 * 60)); + this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d_s / 1000 * 60 * 60)); this.lastUpdate = now; } diff --git a/src/util/amounts.ts b/src/util/amounts.ts index c8fb76793..c85c4839a 100644 --- a/src/util/amounts.ts +++ b/src/util/amounts.ts @@ -1,17 +1,17 @@ /* - This file is part of TALER - (C) 2018 GNUnet e.V. and INRIA + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. - TALER is free software; you can redistribute it and/or modify it under the + 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. - TALER is distributed in the hope that it will be useful, but WITHOUT ANY + 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** @@ -21,7 +21,12 @@ /** * Imports. */ -import { Checkable } from "./checkable"; +import { + typecheckedCodec, + makeCodecForObject, + codecForString, + codecForNumber, +} from "./codec"; /** * Number of fractional units that one value unit represents. @@ -44,29 +49,32 @@ export const maxAmountValue = 2 ** 52; * Non-negative financial amount. Fractional values are expressed as multiples * of 1e-8. */ -@Checkable.Class() -export class AmountJson { +export interface AmountJson { /** * Value, must be an integer. */ - @Checkable.Number() readonly value: number; /** * Fraction, must be an integer. Represent 1/1e8 of a unit. */ - @Checkable.Number() readonly fraction: number; /** * Currency of the amount. */ - @Checkable.String() readonly currency: string; - - static checked: (obj: any) => AmountJson; } +export const codecForAmountJson = () => + typecheckedCodec<AmountJson>( + makeCodecForObject<AmountJson>() + .property("currency", codecForString) + .property("value", codecForNumber) + .property("fraction", codecForNumber) + .build("AmountJson"), + ); + /** * Result of a possibly overflowing operation. */ diff --git a/src/util/checkable.ts b/src/util/checkable.ts deleted file mode 100644 index 3c9fe5bc1..000000000 --- a/src/util/checkable.ts +++ /dev/null @@ -1,417 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - - -/** - * Decorators for validating JSON objects and converting them to a typed - * object. - * - * The decorators are put onto classes, and the validation is done - * via a static method that is filled in by the annotation. - * - * Example: - * ``` - * @Checkable.Class - * class Person { - * @Checkable.String - * name: string; - * @Checkable.Number - * age: number; - * - * // Method will be implemented automatically - * static checked(obj: any): Person; - * } - * ``` - */ -export namespace Checkable { - - type Path = Array<number | string>; - - interface SchemaErrorConstructor { - new (err: string): SchemaError; - } - - interface SchemaError { - name: string; - message: string; - } - - interface Prop { - propertyKey: any; - checker: any; - type?: any; - typeThunk?: () => any; - elementChecker?: any; - elementProp?: any; - keyProp?: any; - stringChecker?: (s: string) => boolean; - valueProp?: any; - optional?: boolean; - } - - interface CheckableInfo { - extraAllowed: boolean; - props: Prop[]; - } - - // tslint:disable-next-line:no-shadowed-variable - export const SchemaError = (function SchemaError(this: any, message: string) { - const that: any = this as any; - that.name = "SchemaError"; - that.message = message; - that.stack = (new Error() as any).stack; - }) as any as SchemaErrorConstructor; - - - SchemaError.prototype = new Error(); - - /** - * Classes that are checkable are annotated with this - * checkable info symbol, which contains the information necessary - * to check if they're valid. - */ - const checkableInfoSym = Symbol("checkableInfo"); - - /** - * Get the current property list for a checkable type. - */ - function getCheckableInfo(target: any): CheckableInfo { - let chk = target[checkableInfoSym] as CheckableInfo|undefined; - if (!chk) { - chk = { props: [], extraAllowed: false }; - target[checkableInfoSym] = chk; - } - return chk; - } - - - function checkNumber(target: any, prop: Prop, path: Path): any { - if ((typeof target) !== "number") { - throw new SchemaError(`expected number for ${path}`); - } - return target; - } - - - function checkString(target: any, prop: Prop, path: Path): any { - if (typeof target !== "string") { - throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`); - } - if (prop.stringChecker && !prop.stringChecker(target)) { - throw new SchemaError(`string property ${path} malformed`); - } - return target; - } - - function checkBoolean(target: any, prop: Prop, path: Path): any { - if (typeof target !== "boolean") { - throw new SchemaError(`expected boolean for ${path}, got ${typeof target} instead`); - } - return target; - } - - - function checkAnyObject(target: any, prop: Prop, path: Path): any { - if (typeof target !== "object") { - throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`); - } - return target; - } - - - function checkAny(target: any, prop: Prop, path: Path): any { - return target; - } - - - function checkList(target: any, prop: Prop, path: Path): any { - if (!Array.isArray(target)) { - throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`); - } - for (let i = 0; i < target.length; i++) { - const v = target[i]; - prop.elementChecker(v, prop.elementProp, path.concat([i])); - } - return target; - } - - function checkMap(target: any, prop: Prop, path: Path): any { - if (typeof target !== "object") { - throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`); - } - for (const key in target) { - prop.keyProp.checker(key, prop.keyProp, path.concat([key])); - const value = target[key]; - prop.valueProp.checker(value, prop.valueProp, path.concat([key])); - } - return target; - } - - - function checkOptional(target: any, prop: Prop, path: Path): any { - console.assert(prop.propertyKey); - prop.elementChecker(target, - prop.elementProp, - path.concat([prop.propertyKey])); - return target; - } - - - function checkValue(target: any, prop: Prop, path: Path): any { - let type; - if (prop.type) { - type = prop.type; - } else if (prop.typeThunk) { - type = prop.typeThunk(); - if (!type) { - throw Error(`assertion failed: typeThunk returned null (prop is ${JSON.stringify(prop)})`); - } - } else { - throw Error(`assertion failed: type/typeThunk missing (prop is ${JSON.stringify(prop)})`); - } - const typeName = type.name || "??"; - const v = target; - if (!v || typeof v !== "object") { - throw new SchemaError( - `expected object for ${path.join(".")}, got ${typeof v} instead`); - } - const chk = type.prototype[checkableInfoSym]; - const props = chk.props; - const remainingPropNames = new Set(Object.getOwnPropertyNames(v)); - const obj = new type(); - for (const innerProp of props) { - if (!remainingPropNames.has(innerProp.propertyKey)) { - if (innerProp.optional) { - continue; - } - throw new SchemaError(`Property '${innerProp.propertyKey}' missing on '${path}' of '${typeName}'`); - } - if (!remainingPropNames.delete(innerProp.propertyKey)) { - throw new SchemaError("assertion failed"); - } - const propVal = v[innerProp.propertyKey]; - obj[innerProp.propertyKey] = innerProp.checker(propVal, - innerProp, - path.concat([innerProp.propertyKey])); - } - - if (!chk.extraAllowed && remainingPropNames.size !== 0) { - const err = `superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`; - throw new SchemaError(err); - } - return obj; - } - - - /** - * Class with checkable annotations on fields. - * This annotation adds the implementation of the `checked` - * static method. - */ - export function Class(opts: {extra?: boolean, validate?: boolean} = {}) { - return (target: any) => { - const chk = getCheckableInfo(target.prototype); - chk.extraAllowed = !!opts.extra; - target.checked = (v: any) => { - const cv = checkValue(v, { - checker: checkValue, - propertyKey: "(root)", - type: target, - }, ["(root)"]); - if (opts.validate) { - if (typeof target.validate !== "function") { - throw Error("invalid Checkable annotion: validate method required"); - } - // May throw exception - target.validate(cv); - } - return cv; - }; - return target; - }; - } - - - /** - * Target property must be a Checkable object of the given type. - */ - export function Value(typeThunk: () => any) { - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkValue, - propertyKey, - typeThunk, - }); - } - - return deco; - } - - - /** - * List of values that match the given annotation. For example, `@Checkable.List(Checkable.String)` is - * an annotation for a list of strings. - */ - export function List(type: any) { - const stub = {}; - type(stub, "(list-element)"); - const elementProp = getCheckableInfo(stub).props[0]; - const elementChecker = elementProp.checker; - if (!elementChecker) { - throw Error("assertion failed"); - } - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkList, - elementChecker, - elementProp, - propertyKey, - }); - } - - return deco; - } - - - /** - * Map from the key type to value type. Takes two annotations, - * one for the key type and one for the value type. - */ - export function Map(keyType: any, valueType: any) { - const keyStub = {}; - keyType(keyStub, "(map-key)"); - const keyProp = getCheckableInfo(keyStub).props[0]; - if (!keyProp) { - throw Error("assertion failed"); - } - const valueStub = {}; - valueType(valueStub, "(map-value)"); - const valueProp = getCheckableInfo(valueStub).props[0]; - if (!valueProp) { - throw Error("assertion failed"); - } - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkMap, - keyProp, - propertyKey, - valueProp, - }); - } - - return deco; - } - - - /** - * Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`. - */ - export function Optional(type: (target: object, propertyKey: string | symbol) => void | any) { - const stub = {}; - type(stub, "(optional-element)"); - const elementProp = getCheckableInfo(stub).props[0]; - const elementChecker = elementProp.checker; - if (!elementChecker) { - throw Error("assertion failed"); - } - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkOptional, - elementChecker, - elementProp, - optional: true, - propertyKey, - }); - } - - return deco; - } - - - /** - * Target property must be a number. - */ - export function Number(): (target: object, propertyKey: string | symbol) => void { - const deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({checker: checkNumber, propertyKey}); - }; - return deco; - } - - - /** - * Target property must be an arbitary object. - */ - export function AnyObject(): (target: object, propertyKey: string | symbol) => void { - const deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkAnyObject, - propertyKey, - }); - }; - return deco; - } - - - /** - * Target property can be anything. - * - * Not useful by itself, but in combination with higher-order annotations - * such as List or Map. - */ - export function Any(): (target: object, propertyKey: string | symbol) => void { - const deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkAny, - optional: true, - propertyKey, - }); - }; - return deco; - } - - - /** - * Target property must be a string. - */ - export function String( - stringChecker?: (s: string) => boolean): (target: object, propertyKey: string | symbol, - ) => void { - const deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({ checker: checkString, propertyKey, stringChecker }); - }; - return deco; - } - - /** - * Target property must be a boolean value. - */ - export function Boolean(): (target: object, propertyKey: string | symbol) => void { - const deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({ checker: checkBoolean, propertyKey }); - }; - return deco; - } -} diff --git a/src/util/codec.ts b/src/util/codec.ts index a13816c59..e18a5e74c 100644 --- a/src/util/codec.ts +++ b/src/util/codec.ts @@ -32,11 +32,11 @@ export class DecodingError extends Error { /** * Context information to show nicer error messages when decoding fails. */ -interface Context { +export interface Context { readonly path?: string[]; } -function renderContext(c?: Context): string { +export function renderContext(c?: Context): string { const p = c?.path; if (p) { return p.join("."); @@ -84,6 +84,9 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { x: K, codec: Codec<V>, ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> { + if (!codec) { + throw Error("inner codec must be defined"); + } this.propList.push({ name: x, codec: codec }); return this as any; } @@ -143,6 +146,9 @@ class UnionCodecBuilder< CommonBaseType, PartialTargetType | V > { + if (!codec) { + throw Error("inner codec must be defined"); + } this.alternatives.set(tagValue, { codec, tagValue }); return this as any; } @@ -215,6 +221,9 @@ export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> { export function makeCodecForMap<T>( innerCodec: Codec<T>, ): Codec<{ [x: string]: T }> { + if (!innerCodec) { + throw Error("inner codec must be defined"); + } return { decode(x: any, c?: Context): { [x: string]: T } { const map: { [x: string]: T } = {}; @@ -233,6 +242,9 @@ export function makeCodecForMap<T>( * Return a codec for a list, containing values described by the inner codec. */ export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> { + if (!innerCodec) { + throw Error("inner codec must be defined"); + } return { decode(x: any, c?: Context): T[] { const arr: T[] = []; @@ -255,7 +267,19 @@ export const codecForNumber: Codec<number> = { if (typeof x === "number") { return x; } - throw new DecodingError(`expected number at ${renderContext(c)}`); + throw new DecodingError(`expected number at ${renderContext(c)} but got ${typeof x}`); + }, +}; + +/** + * Return a codec for a value that must be a number. + */ +export const codecForBoolean: Codec<boolean> = { + decode(x: any, c?: Context): boolean { + if (typeof x === "boolean") { + return x; + } + throw new DecodingError(`expected boolean at ${renderContext(c)} but got ${typeof x}`); }, }; @@ -267,7 +291,16 @@ export const codecForString: Codec<string> = { if (typeof x === "string") { return x; } - throw new DecodingError(`expected string at ${renderContext(c)}`); + throw new DecodingError(`expected string at ${renderContext(c)} but got ${typeof x}`); + }, +}; + +/** + * Codec that allows any value. + */ +export const codecForAny: Codec<any> = { + decode(x: any, c?: Context): any { + return x; }, }; @@ -281,12 +314,23 @@ export function makeCodecForConstString<V extends string>(s: V): Codec<V> { return x; } throw new DecodingError( - `expected string constant "${s}" at ${renderContext(c)}`, + `expected string constant "${s}" at ${renderContext(c)} but got ${typeof x}`, ); }, }; } +export function makeCodecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> { + return { + decode(x: any, c?: Context): V | undefined { + if (x === undefined || x === null) { + return undefined; + } + return innerCodec.decode(x, c); + } + } +} + export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> { return c; } diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 8136f44fa..722688d35 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -24,8 +24,6 @@ import { AmountJson } from "./amounts"; import * as Amounts from "./amounts"; -import { Timestamp, Duration } from "../types/walletTypes"; - /** * Show an amount in a form suitable for the user. * FIXME: In the future, this should consider currency-specific @@ -114,75 +112,6 @@ export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] { return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []); } - -/** - * Extract a numeric timstamp (in seconds) from the Taler date format - * ("/Date([n])/"). Returns null if input is not in the right format. - */ -export function getTalerStampSec(stamp: string): number | null { - const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); - if (!m || !m[1]) { - return null; - } - return parseInt(m[1], 10); -} - -/** - * Extract a timestamp from a Taler timestamp string. - */ -export function extractTalerStamp(stamp: string): Timestamp | undefined { - const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); - if (!m || !m[1]) { - return undefined; - } - return { - t_ms: parseInt(m[1], 10) * 1000, - }; -} - -/** - * Extract a timestamp from a Taler timestamp string. - */ -export function extractTalerStampOrThrow(stamp: string): Timestamp { - const r = extractTalerStamp(stamp); - if (!r) { - throw Error("invalid time stamp"); - } - return r; -} - -/** - * Extract a duration from a Taler duration string. - */ -export function extractTalerDuration(duration: string): Duration | undefined { - const m = duration.match(/\/?Delay\(([0-9]*)\)\/?/); - if (!m || !m[1]) { - return undefined; - } - return { - d_ms: parseInt(m[1], 10) * 1000, - }; -} - -/** - * Extract a duration from a Taler duration string. - */ -export function extractTalerDurationOrThrow(duration: string): Duration { - const r = extractTalerDuration(duration); - if (!r) { - throw Error("invalid duration"); - } - return r; -} - -/** - * Check if a timestamp is in the right format. - */ -export function timestampCheck(stamp: string): boolean { - return getTalerStampSec(stamp) !== null; -} - - /** * Compute the hash function of a JSON object. */ diff --git a/src/util/time.ts b/src/util/time.ts new file mode 100644 index 000000000..54d22bf81 --- /dev/null +++ b/src/util/time.ts @@ -0,0 +1,165 @@ +import { Codec, renderContext, Context } from "./codec"; + +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Helpers for relative and absolute time. + */ + +export class Timestamp { + /** + * Timestamp in milliseconds. + */ + readonly t_ms: number | "never"; +} + +export interface Duration { + /** + * Duration in milliseconds. + */ + readonly d_ms: number | "forever"; +} + +export function getTimestampNow(): Timestamp { + return { + t_ms: new Date().getTime(), + }; +} + +export function getDurationRemaining( + deadline: Timestamp, + now = getTimestampNow(), +): 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 timestampMin(t1: Timestamp, t2: Timestamp): Timestamp { + if (t1.t_ms === "never") { + return { t_ms: t2.t_ms }; + } + if (t2.t_ms === "never") { + return { t_ms: t2.t_ms }; + } + return { t_ms: Math.min(t1.t_ms, t2.t_ms) }; +} + +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: Math.min(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; + } + 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) { + 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 const codecForTimestamp: Codec<Timestamp> = { + decode(x: any, c?: Context): Timestamp { + const t_ms = x.t_ms; + if (typeof t_ms === "string") { + if (t_ms === "never") { + return { t_ms: "never" }; + } + throw Error(`expected timestamp at ${renderContext(c)}`); + } + if (typeof t_ms === "number") { + return { t_ms }; + } + throw Error(`expected 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" }; + } + throw Error(`expected duration at ${renderContext(c)}`); + } + if (typeof d_ms === "number") { + return { d_ms }; + } + throw Error(`expected duration at ${renderContext(c)}`); + }, +}; diff --git a/src/util/timer.ts b/src/util/timer.ts index 865c17faf..000f36608 100644 --- a/src/util/timer.ts +++ b/src/util/timer.ts @@ -1,17 +1,19 @@ +import { Duration } from "./time"; + /* - This file is part of TALER - (C) 2017 GNUnet e.V. + This file is part of GNU Taler + (C) 2017-2019 Taler Systems S.A. - TALER is free software; you can redistribute it and/or modify it under the + 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. - TALER is distributed in the hope that it will be useful, but WITHOUT ANY + 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** @@ -105,11 +107,13 @@ export class TimerGroup { } } - resolveAfter(delayMs: number): Promise<void> { + resolveAfter(delayMs: Duration): Promise<void> { return new Promise<void>((resolve, reject) => { - this.after(delayMs, () => { - resolve(); - }); + if (delayMs.d_ms !== "forever") { + this.after(delayMs.d_ms, () => { + resolve(); + }); + } }); } |