@lf-lang/reactor-ts
Version:
A reactor-oriented programming framework in TypeScript
626 lines • 22.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Alarm = exports.Origin = exports.Tag = exports.TimeValue = exports.TimeUnit = void 0;
exports.getCurrentPhysicalTime = getCurrentPhysicalTime;
/**
* Time-related helper functions for reactors.
* @author Marten Lohstroh (marten@berkeley.edu)
* @author Matt Weber (matt.weber@berkeley.edu)
*/
const microtime_1 = __importDefault(require("microtime"));
/**
* Units (and conversion factors from nanoseconds) for time values.
**/
var TimeUnit;
(function (TimeUnit) {
TimeUnit[TimeUnit["nsec"] = 1] = "nsec";
TimeUnit[TimeUnit["usec"] = 1000] = "usec";
TimeUnit[TimeUnit["msec"] = 1000000] = "msec";
TimeUnit[TimeUnit["sec"] = 1000000000] = "sec";
TimeUnit[TimeUnit["minute"] = 60000000000] = "minute";
TimeUnit[TimeUnit["hour"] = 3600000000000] = "hour";
TimeUnit[TimeUnit["day"] = 86400000000000] = "day";
TimeUnit[TimeUnit["week"] = 604800000000000] = "week";
})(TimeUnit || (exports.TimeUnit = TimeUnit = {}));
/**
* A time value given in nanosecond precision. To prevent overflow (which would
* occur for time intervals spanning more than 0.29 years if a single JavaScript
* number, which has 2^53 bits of precision, were to be used), we use _two_
* numbers to store a time value. The first number denotes the number of whole
* seconds in the interval; the second number denotes the remaining number of
* nanoseconds in the interval. This class serves as a base class for
* `UnitBasedTimeValue`, which provides the convenience of defining time values
* as a single number accompanied by a unit.
* @see TimeUnit
* @see UnitBasedTimeValue
*/
class TimeValue {
seconds;
nanoseconds;
/**
* Create a new time value. Both parameters must be non-negative integers;
* an error will be thrown otherwise. The second parameter is optional.
* @param seconds Number of seconds in the interval.
* @param nanoseconds Remaining number of nanoseconds (defaults to zero).
*/
constructor(seconds, nanoseconds = 0) {
this.seconds = seconds;
this.nanoseconds = nanoseconds;
if (!Number.isInteger(seconds) ||
!Number.isInteger(nanoseconds) ||
seconds < 0 ||
nanoseconds < 0) {
if (seconds !== Number.MIN_SAFE_INTEGER) {
throw new Error("Cannot instantiate a time interval based on negative or non-integer numbers.");
}
}
}
static zero() {
return new TimeValue(0, 0);
}
static never() {
return new TimeValue(Number.MIN_SAFE_INTEGER, 0);
}
static forever() {
return new TimeValue(Number.MAX_SAFE_INTEGER, 0);
}
static secsAndNs(seconds, nanoSeconds) {
return new TimeValue(seconds, nanoSeconds);
}
static secs(seconds) {
return TimeValue.secsAndNs(seconds, 0);
}
static sec(seconds) {
return TimeValue.secsAndNs(seconds, 0);
}
static msecs(microseconds) {
return TimeValue.withUnits(microseconds, TimeUnit.msec);
}
static msec(microseconds) {
return TimeValue.withUnits(microseconds, TimeUnit.msec);
}
static usecs(nanoseconds) {
return TimeValue.withUnits(nanoseconds, TimeUnit.usec);
}
static usec(nanoseconds) {
return TimeValue.withUnits(nanoseconds, TimeUnit.usec);
}
static nsecs(nanoseconds) {
return TimeValue.withUnits(nanoseconds, TimeUnit.nsec);
}
static nsec(nanoseconds) {
return TimeValue.withUnits(nanoseconds, TimeUnit.nsec);
}
// FIXME: Add more convenience methods
/**
* Return a new time value that denotes the duration of the time interval
* encoded by this time value plus the time interval encoded by the time
* value given as a parameter.
* @param other The time value to add to this one.
*/
add(other) {
if ((this.isNever() && other.isForever()) ||
(this.isForever() && other.isNever())) {
// The sum of minus infinity and infinity is zero.
return TimeValue.zero();
}
else if (this.isNever() || other.isNever()) {
// The sum of minus infinity and a normal tag is minus infinity.
return TimeValue.never();
}
else if (this.isForever() || other.isForever()) {
// The sum of infinity and a normal tag is infinity.
return TimeValue.forever();
}
let seconds = this.seconds + other.seconds;
let nanoseconds = this.nanoseconds + other.nanoseconds;
if (nanoseconds >= TimeUnit.sec.valueOf()) {
// Carry the second.
seconds += 1;
nanoseconds -= TimeUnit.sec;
}
// Check an overflow.
if (!Number.isSafeInteger(seconds)) {
// The overflow happens.
return TimeValue.forever();
}
else {
return TimeValue.secsAndNs(seconds, nanoseconds);
}
}
/**
* Return a new time value that denotes the duration of the time interval
* encoded by this time value minus the time interval encoded by the time
* value given as a parameter.
* @param other The time value to subtract from this one.
*/
subtract(other) {
let s = this.seconds - other.seconds;
let ns = this.nanoseconds - other.nanoseconds;
if (ns < 0) {
// Borrow a second
s -= 1;
ns += TimeUnit.sec;
}
if (s < 0) {
throw new Error("Negative time value.");
}
return TimeValue.secsAndNs(s, ns);
}
multiply(factor) {
let seconds = this.seconds * factor;
let nanoseconds = this.nanoseconds * factor;
if (nanoseconds >= TimeUnit.sec.valueOf()) {
// Carry seconds.
const carry = Math.floor(nanoseconds / TimeUnit.sec);
seconds += carry;
nanoseconds -= carry * TimeUnit.sec;
}
return TimeValue.secsAndNs(seconds, nanoseconds);
}
difference(other) {
if (this.isEarlierThan(other)) {
return other.subtract(this);
}
else {
return this.subtract(other);
}
}
/**
* Return true if this time value denotes a time interval of equal length as
* the interval encoded by the time value given as a parameter.
* @param other The time value to compare to this one.
*/
isEqualTo(other) {
return (this.seconds === other.seconds && this.nanoseconds === other.nanoseconds);
}
/**
* Return true if this denotes a time interval of length zero.
*/
isZero() {
if (this.seconds === 0 && this.nanoseconds === 0) {
return true;
}
else {
return false;
}
}
isNever() {
if (this.seconds === Number.MIN_SAFE_INTEGER && this.nanoseconds === 0) {
return true;
}
else {
return false;
}
}
isForever() {
if (this.seconds === Number.MAX_SAFE_INTEGER) {
return true;
}
else {
return false;
}
}
/**
* Return true if this time value denotes a time interval of smaller length
* than the time interval encoded by the time value given as a parameter;
* return false otherwise.
*
* NOTE: Performing this comparison involves a conversion to a big integer
* and is therefore relatively costly.
*
* @param other The time value to compare to this one.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt|BigInt} for further information.
*/
isEarlierThan(other) {
if (this.seconds < other.seconds) {
return true;
}
if (this.seconds === other.seconds &&
this.nanoseconds < other.nanoseconds) {
return true;
}
return false;
}
/**
* Return true if this time value is later than the time given as a parameter.
*
* @param other The time value to compare to this one.
*/
isLaterThan(other) {
if (this.seconds > other.seconds) {
return true;
}
if (this.seconds === other.seconds &&
this.nanoseconds > other.nanoseconds) {
return true;
}
return false;
}
/**
* Return a millisecond representation of this time value.
*/
toMilliseconds() {
return this.seconds * 1000 + Math.ceil(this.nanoseconds / 1000000);
}
/**
* Print the number of seconds and nanoseconds in the time interval encoded
* by this time value.
*/
toString() {
return `(${this.seconds} secs; ${this.nanoseconds} nsecs)`;
}
/**
* Return a tuple that holds the seconds and remaining nanoseconds that
* jointly represent this time value.
*/
toTimeTuple() {
return [this.seconds, this.nanoseconds];
}
/**
* Get a 64 bit binary, little endian representation of this TimeValue.
* Used by federates.
*/
toBinary() {
const buff = Buffer.alloc(8);
if (this.seconds === Number.MIN_SAFE_INTEGER) {
buff.writeBigUInt64LE(BigInt(0x8000000000000000n), 0);
}
else if (this.seconds === Number.MAX_SAFE_INTEGER) {
buff.writeBigUInt64LE(BigInt(0x7fffffffffffffffn), 0);
}
else {
const billion = BigInt(TimeUnit.sec);
const bigTime = BigInt(this.nanoseconds) + BigInt(this.seconds) * billion;
// Ensure the TimeValue fits into a 64 unsigned integer.
const clampedTime = BigInt.asUintN(64, bigTime);
if (clampedTime !== bigTime) {
throw new Error(`TimeValue ${this.toString()} is too big to fit into ` +
"a 64 bit unsigned integer");
}
buff.writeBigUInt64LE(bigTime, 0);
}
return buff;
}
/**
* Create a TimeValue from a 64bit little endian unsigned integer in a buffer.
* @param buffer A 64 bit unsigned integer. Little endian.
*/
static fromBinary(buffer) {
const billion = BigInt(TimeUnit.sec);
// To avoid overflow and floating point errors, work with BigInts.
const bigTime = buffer.readBigUInt64LE(0);
if (bigTime === BigInt(0x8000000000000000n)) {
return TimeValue.never();
}
else if (bigTime === BigInt(0x7fffffffffffffffn)) {
return TimeValue.forever();
}
const bigSeconds = bigTime / billion;
const bigNSeconds = bigTime % billion;
return TimeValue.secsAndNs(Number(bigSeconds), Number(bigNSeconds));
}
/**
* Give a value and time unit, return a new time value.
*
* The value is a `number` that is required to be
* a positive integer. The time unit must be a member of the `TimeUnit` enum.
* @param value A number (which must be a positive integer) that denotes
* the length of the specified time interval, expressed as a multiple of
* the given time unit.
* @param unit The unit of measurement that applies to the given value.
* @returns
*/
static withUnits(value, unit) {
if (!Number.isInteger(value)) {
throw new Error("Non-integer time values are illegal.");
}
if (value < 0) {
throw new Error("Negative time values are illegal.");
}
const billion = BigInt(TimeUnit.sec);
// To avoid overflow and floating point errors, work with BigInts.
const bigT = BigInt(value) * BigInt(unit);
const bigSeconds = bigT / billion;
if (bigSeconds > Number.MAX_SAFE_INTEGER) {
throw new Error("Unable to instantiate time value: value too large.");
}
return TimeValue.secsAndNs(Number(bigSeconds), Number(bigT % billion));
}
}
exports.TimeValue = TimeValue;
/**
* A superdense time instant, represented as a time value `time` (i.e.,
* time elapsed since Epoch) paired with a microstep index to keep track of
* iterations in superdense time. For each such iteration, `time` remains
* the same, but `microstep` is incremented.
*/
class Tag {
microstep;
/**
* Time elapsed since Epoch.
*/
time;
/**
* Create a new tag using a time value and a microstep.
* @param timeSinceEpoch Time elapsed since Epoch.
* @param microstep Superdense time index.
*/
constructor(timeSinceEpoch, microstep = 0) {
this.microstep = microstep;
if (!Number.isInteger(microstep)) {
throw new Error("Microstep must be integer.");
}
if (microstep < 0) {
throw new Error("Microstep must be positive.");
}
this.time = timeSinceEpoch;
}
/**
* Return `true` if this time instant is earlier than the tag given as a
* parameter, false otherwise. For two tags with an equal `time`, one
* instant is earlier than the other if its `microstep` is less than the
* `microstep` of the other.
* @param other The time instant to compare against this one.
*/
isSmallerThan(other) {
return (this.time.isEarlierThan(other.time) ||
(this.time.isEqualTo(other.time) && this.microstep < other.microstep));
}
/**
* Return `true` if the tag is smaller than or equal to the tag given as a parameter.
* @param other The time instant to compare against this one.
*/
isSmallerThanOrEqualTo(other) {
return !this.isGreaterThan(other);
}
/**
* Return `true` if the tag is greater than the tag given as a parameter.
* @param other The time instant to compare against this one.
*/
isGreaterThan(other) {
return (this.time.isLaterThan(other.time) ||
(this.time.isEqualTo(other.time) && this.microstep > other.microstep));
}
/**
* Return `true` if the tag is greater than or equal to the tag given as a parameter.
* @param other The time instant to compare against this one.
*/
isGreaterThanOrEqualTo(other) {
return !this.isSmallerThan(other);
}
/**
* Return `true` if this tag is simultaneous with the tag given as
* a parameter, false otherwise. Both `time` and `microstep` must be equal
* for two tags to be simultaneous.
* @param other The time instant to compare against this one.
*/
isSimultaneousWith(other) {
return (this.time.isEqualTo(other.time) && this.microstep === other.microstep);
}
/**
* Get a new time instant that represents this time instant plus
* the given delay. The `microstep` of this time instant is ignored;
* the returned time instant has a `microstep` of zero if the delay
* is greater than zero. If the delay equals zero or is undefined,
* the tag is returned unchanged with its current `microstep`.
* @param delay The time interval to add to this time instant.
*/
getLaterTag(delay) {
if (delay === undefined) {
return this;
}
else if (delay.isZero()) {
return new Tag(this.time, this.microstep + 1);
}
else {
return new Tag(delay.add(this.time), 0);
}
}
/**
* Get a new time instant that has the same `time` but n `microsteps` later.
*/
getMicroStepsLater(n) {
return new Tag(this.time, this.microstep + n);
}
/**
* Obtain a time interval that represents the absolute (i.e., positive) time
* difference between this time interval and the tag given as a parameter.
* @param other The time instant for which to compute the absolute
* difference with this time instant.
*/
getTimeDifference(other) {
if (this.isSmallerThan(other)) {
return other.time.subtract(this.time);
}
else {
return this.time.subtract(other.time);
}
}
/**
* Return a human-readable string presentation of this time instant.
*/
toString() {
return `(${this.time.toString()}, ${this.microstep})`;
}
/**
* Get a 12-byte buffer for the tag.
* Used by federates.
*/
toBinary() {
const timeBuffer = this.time.toBinary();
const buf = Buffer.alloc(12);
timeBuffer.copy(buf, 0, 0);
buf.writeInt32LE(this.microstep, 8);
return buf;
}
/**
* Create a Tag from 8-byte TimeValue and 4-byte microstep in a buffer.
* @param buffer A buffer of TimeValue and microstep.
*/
static fromBinary(buffer) {
const timeBuffer = Buffer.alloc(8);
buffer.copy(timeBuffer, 0, 0, 8);
const time = TimeValue.fromBinary(timeBuffer);
const microstep = buffer.readUInt32LE(8);
return new Tag(time, microstep);
}
}
exports.Tag = Tag;
/**
* A descriptor to be used when scheduling events to denote as to whether
* an event should occur with a delay relative to _physical time_ or _logical
* time_.
*/
var Origin;
(function (Origin) {
Origin["physical"] = "physical";
Origin["logical"] = "logical";
})(Origin || (exports.Origin = Origin = {}));
/**
* Return a time value that reflects the current physical time as reported
* by the platform.
*/
function getCurrentPhysicalTime() {
const t = microtime_1.default.now();
const seconds = Math.floor(t / 1000000);
const nseconds = t * 1000 - seconds * TimeUnit.sec;
return TimeValue.secsAndNs(seconds, nseconds);
}
/**
* Simple but accurate alarm that makes use of high-resolution timer.
* The algorithm is inspired by nanotimer, written by Kevin Briggs.
* @author Marten Lohstroh (marten@berkeley.edu)
*/
class Alarm {
/**
* Handle for regular timeout, used for delays > 25 ms.
*/
deferredRef;
/**
* Handle for immediate, used for polling once the remaining delay is < 25
* ms.
*/
immediateRef;
/**
* Delay in terms of milliseconds, used when deferring to regular timeout.
*/
loResDelay = 0;
/**
* Start of the delay interval; tuple of seconds and nanoseconds.
*/
hiResStart = [0, 0];
/**
* The delay interval in high resolution; tuple of seconds and nanoseconds.
*/
hiResDelay = [0, 0];
/**
* Indicates whether the alarm has been set or not.
*/
active = false;
/**
* Disable any scheduled timeouts or immediate events, and set the timer to
* inactive.
*/
unset() {
if (this.deferredRef != null) {
clearTimeout(this.deferredRef);
this.deferredRef = undefined;
}
if (this.immediateRef != null) {
clearImmediate(this.immediateRef);
this.immediateRef = undefined;
}
this.active = false;
}
/**
* Once the alarm has been initialized, see if the task can be performed or
* a longer wait is necessary.
* @param task The task to perform.
* @param callback Optional callback used to report the wait time.
*/
try(task, callback) {
// Record the current time.
const hiResDif = process.hrtime(this.hiResStart);
// See whether the requested delay has elapsed.
if (this.hiResDelay[0] < hiResDif[0] ||
(this.hiResDelay[0] === hiResDif[0] && this.hiResDelay[1] < hiResDif[1])) {
// No more immediates a scheduled.
this.immediateRef = undefined;
// The delay has elapsed; perform the task.
if (this.active) {
// If this attempt is the result of a deferred request, perform
// the task synchronously.
this.active = false;
task();
if (callback != null) {
callback(TimeValue.secsAndNs(...hiResDif));
}
}
else {
// If this attempt is the result of a direct call to `set`, push
// task onto the nextTick queue. This unwinds the stack, but it
// bypasses the regular event queue. If events are recursively
// requested to be performed with zero or near zero delay, this
// will not cause the call stack to exceed its maximum size, but
// it will starve I/O.
this.active = true;
process.nextTick(() => {
if (this.active) {
this.active = false;
task();
if (callback != null) {
callback(TimeValue.secsAndNs(...hiResDif));
}
}
});
}
}
else {
// The delay has not yet elapsed.
// The following logic is based on the implementation of nanotimer.
if (this.loResDelay > 25) {
if (!this.active) {
this.deferredRef = setTimeout(() => {
this.try(task, callback);
}, this.loResDelay - 25);
}
else {
this.deferredRef = undefined;
this.immediateRef = setImmediate(() => {
this.try(task, callback);
});
}
}
else {
this.immediateRef = setImmediate(() => {
this.try(task, callback);
});
}
this.active = true;
}
}
/**
* Set the alarm.
* @param task The task to be performed.
* @param delay The time has to elapse before the task can be performed.
* @param callback Optional callback used to report the wait time.
*/
set(task, delay, callback) {
// Reset the alarm if it was already active.
if (this.active) {
this.unset();
}
// Compute the delay
this.hiResDelay = delay.toTimeTuple();
this.loResDelay = delay.toMilliseconds();
// Record the beginning of the delay interval.
this.hiResStart = process.hrtime();
this.try(task, callback);
}
}
exports.Alarm = Alarm;
//# sourceMappingURL=time.js.map