UNPKG

@agoric/time

Version:

Timestamps, time math, timer service API definition

269 lines (243 loc) • 8.44 kB
import { Nat } from '@endo/nat'; import { Fail, q } from '@endo/errors'; import { mustMatch } from '@agoric/store'; import { RelativeTimeRecordShape, TimestampRecordShape } from './typeGuards.js'; /** * @import {RelativeTime, RelativeTimeValue, TimerBrand, TimeMathType, Timestamp, TimestampRecord, TimestampValue} from './types.js'; */ /** * `agreedTimerBrand` is internal to this module. * * @param {TimerBrand | undefined} leftBrand * @param {TimerBrand | undefined} rightBrand * @returns {TimerBrand | undefined} */ const agreedTimerBrand = (leftBrand, rightBrand) => { if (leftBrand === undefined) { if (rightBrand === undefined) { return undefined; } else { return rightBrand; } } else if (rightBrand === undefined) { return leftBrand; } else { leftBrand === rightBrand || Fail`TimerBrands must match: ${q(leftBrand)} vs ${q(rightBrand)}`; return leftBrand; } }; /** * `sharedTimerBrand` is internal to this module, and implements the * transitional brand checking and contaigion logic explained in the `TimeMath` * comment. It is used to define the binary operators that should follow * this logic. It does the error checking between the operands, and returns * the brand, if any, that should label the resulting time value. * * @param {Timestamp | RelativeTime} left * @param {Timestamp | RelativeTime} right * @returns {TimerBrand | undefined} */ const sharedTimerBrand = (left, right) => { const leftBrand = typeof left === 'bigint' ? undefined : left.timerBrand; const rightBrand = typeof right === 'bigint' ? undefined : right.timerBrand; return agreedTimerBrand(leftBrand, rightBrand); }; /** * `absLike` is internal to this module, and used to implement the binary * operators in the case where the returned time should be a `Timestamp` * rather than a `RelativeTime`. * * @param {Timestamp | RelativeTime} left * @param {Timestamp | RelativeTime} right * @param {TimestampValue} absValue * @returns {Timestamp} */ const absLike = (left, right, absValue) => { Nat(absValue); const timerBrand = sharedTimerBrand(left, right); if (timerBrand) { return harden({ timerBrand, absValue, }); } else { return absValue; } }; /** * `relLike` is internal to this module, and used to implement the binary * operators in the case where the returned time should be a `RelativeTime` * rather than a `Timestamp`. * * @param {Timestamp | RelativeTime} left * @param {Timestamp | RelativeTime} right * @param {RelativeTimeValue} relValue * @returns {RelativeTime} */ const relLike = (left, right, relValue) => { Nat(relValue); const timerBrand = sharedTimerBrand(left, right); if (timerBrand) { return harden({ timerBrand, relValue, }); } else { return relValue; } }; // For all the following time operators, their documentation is in // the `TimeMathType`, since that is the documentation that shows up // in the IDE. Well, at least the vscode IDE. const absValue = abs => { if (typeof abs === 'bigint') { return Nat(abs); } mustMatch(abs, TimestampRecordShape, 'timestamp'); return Nat(abs.absValue); }; const relValue = rel => { if (typeof rel === 'bigint') { return Nat(rel); } mustMatch(rel, RelativeTimeRecordShape, 'relative'); return Nat(rel.relValue); }; const makeTimestampRecord = (abs, timerBrand) => harden({ absValue: abs, timerBrand }); const makeRelativeTimeRecord = (rel, timerBrand) => harden({ relValue: rel, timerBrand }); const coerceTimestampRecord = (ts, brand) => { brand || Fail`must have a brand`; if (typeof ts === 'number') { ts = Nat(ts); } if (typeof ts === 'bigint') { return makeTimestampRecord(ts, brand); } else { const { timerBrand } = ts; mustMatch(ts, TimestampRecordShape, 'timestamp'); agreedTimerBrand(timerBrand, brand); return ts; } }; const coerceRelativeTimeRecord = (rt, brand) => { brand || Fail`must have a brand`; if (typeof rt === 'number') { rt = Nat(rt); } if (typeof rt === 'bigint') { return makeRelativeTimeRecord(rt, brand); } else { const { timerBrand } = rt; mustMatch(rt, RelativeTimeRecordShape, 'relativeTime'); agreedTimerBrand(timerBrand, brand); return rt; } }; const addAbsRel = (abs, rel) => absLike(abs, rel, absValue(abs) + relValue(rel)); const addRelRel = (rel1, rel2) => relLike(rel1, rel2, relValue(rel1) + relValue(rel2)); const subtractAbsAbs = (abs1, abs2) => relLike(abs1, abs2, absValue(abs1) - absValue(abs2)); const clampedSubtractAbsAbs = (abs1, abs2) => { const val1 = absValue(abs1); const val2 = absValue(abs2); return relLike(abs1, abs2, val1 > val2 ? val1 - val2 : 0n); }; const subtractAbsRel = (abs, rel) => absLike(abs, rel, absValue(abs) - relValue(rel)); const subtractRelRel = (rel1, rel2) => relLike(rel1, rel2, relValue(rel1) - relValue(rel2)); const isRelZero = rel => relValue(rel) === 0n; const multiplyRelNat = (rel, nat) => relLike(rel, nat, relValue(rel) * nat); const divideRelNat = (rel, nat) => relLike(rel, nat, relValue(rel) / nat); const divideRelRel = (rel1, rel2) => { sharedTimerBrand(rel1, rel2); // just error check return relValue(rel1) / relValue(rel2); }; const modAbsRel = (abs, step) => relLike(abs, step, absValue(abs) % relValue(step)); const modRelRel = (rel, step) => relLike(rel, step, relValue(rel) % relValue(step)); /** * `compareValues` is internal to this module, and used to implement * the time comparison operators. * * @param {Timestamp | RelativeTime} left * @param {Timestamp | RelativeTime} right * @param {bigint} v1 * @param {bigint} v2 * @returns {import('@endo/marshal').RankComparison} */ const compareValues = (left, right, v1, v2) => { sharedTimerBrand(left, right); if (v1 < v2) { return -1; } else if (v1 === v2) { return 0; } else { assert(v1 > v2); return 1; } }; /** * The `TimeMath` object provides helper methods to do arithmetic on labeled * time values, much like `AmountMath` provides helper methods to do arithmetic * on labeled asset/money values. Both check for consistency of labels: a * binary operation on two labeled objects ensures that the both carry * the same label. If they produce another object from the same domain, it * will carry the same label. If the operands have incompatible labels, * an error is thrown. * * Unlike amount arithmetic, time arithmetic deals in two kinds of time objects: * Timestamps, which represent absolute time, and RelativeTime, which represents * the duration between two absolute times. Both kinds of time object * are labeled by a `TimerBrand`. For a Timestamp object, the value is * a bigint in an `absValue` property. For a RelativeTime object, the value * is a bigint in a `relValue` property. Thus we have a runtime safety check * to ensure that we don't confused the two, even if we have managed to fool * the (unsound) static type system. * * As a transitional measure, currently many Timestamps and RelativeTimes are * still represented by unlabeled bigints. During this transitional period, * we allow this, both statically and dynamically. For a normal binary * operation, if both inputs are labeled, then we do the full checking as * explained above and return a labeled result. If both inputs are unlabeled * bigints, we *assume* that they indicate a time of the right kind * (Timestamp vs RelativeTime) and timer brand. Since we don't know what * brand was intended, we can only return yet another unlabeled bigint. * * If one operand is labeled and the other is not, we check the labeled operand, * *assume* the unlabeled bigint represents the value needed for the other * operand, and return a labeled time object with the brand of the labeled * operand. * * @type {TimeMathType} */ export const TimeMath = harden({ absValue, relValue, coerceTimestampRecord, coerceRelativeTimeRecord, // @ts-expect-error xxx dynamic typing addAbsRel, // @ts-expect-error xxx dynamic typing addRelRel, subtractAbsAbs, clampedSubtractAbsAbs, subtractAbsRel, subtractRelRel, isRelZero, multiplyRelNat, divideRelNat, divideRelRel, modAbsRel, modRelRel, compareAbs: (abs1, abs2) => compareValues(abs1, abs2, absValue(abs1), absValue(abs2)), compareRel: (rel1, rel2) => compareValues(rel1, rel2, relValue(rel1), relValue(rel2)), });