mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
feat: create branded types to determine different handling of time
This commit is contained in:
committed by
Carlos Valente
parent
168d7103b1
commit
e8f159d894
@@ -0,0 +1,185 @@
|
||||
import { dayInMs, millisToString } from 'ontime-utils';
|
||||
import { Duration, Instant } from 'ontime-types';
|
||||
|
||||
import { timeNow } from '../../../utils/time.js';
|
||||
import * as timeCore from '../timeCore.js';
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// TZ is set to Europe/Copenhagen in vitest.global-setup.ts
|
||||
// Copenhagen is UTC+1 (CET) in winter and UTC+2 (CEST) in summer
|
||||
|
||||
describe('toTimeofDay() converts an instant to local milliseconds since midnight', () => {
|
||||
const testTimes = [
|
||||
{ time: '2025-01-15T08:30:00Z', label: 'winter morning' },
|
||||
{ time: '2025-06-15T14:00:00Z', label: 'summer afternoon' },
|
||||
{ time: '2025-01-01T00:00:00Z', label: 'midnight UTC on new years' },
|
||||
{ time: '2025-07-01T23:59:59Z', label: 'just before midnight UTC in summer' },
|
||||
{ time: '2025-03-15T12:00:00Z', label: 'noon UTC in winter' },
|
||||
{ time: '2025-09-15T12:00:00Z', label: 'noon UTC in summer' },
|
||||
];
|
||||
|
||||
test.each(testTimes)('produces the correct local time for $label ($time)', ({ time }) => {
|
||||
vi.setSystemTime(time);
|
||||
|
||||
const result = timeCore.toTimeOfDay(timeCore.now());
|
||||
expect(result).toBe(timeNow());
|
||||
});
|
||||
|
||||
it('returns a value in the range [0, dayInMs)', () => {
|
||||
vi.setSystemTime('2025-06-15T23:59:59.999Z');
|
||||
|
||||
const result = timeCore.toTimeOfDay(timeCore.now());
|
||||
expect(result).toBeGreaterThanOrEqual(0);
|
||||
expect(result).toBeLessThan(dayInMs);
|
||||
});
|
||||
|
||||
describe('handles DST transitions in Europe/Copenhagen', () => {
|
||||
it('produces CET time just before spring forward', () => {
|
||||
// 2025-03-30 at 02:00 CET clocks jump to 03:00 CEST
|
||||
// UTC 00:58:18 → Copenhagen CET (UTC+1) → local 01:58:18
|
||||
vi.setSystemTime('2025-03-30T00:58:18Z');
|
||||
|
||||
const result = timeCore.toTimeOfDay(timeCore.now());
|
||||
expect(millisToString(result)).toBe('01:58:18');
|
||||
});
|
||||
|
||||
it('produces CEST time just after spring forward', () => {
|
||||
// UTC 01:00:00 → Copenhagen CEST (UTC+2) → local 03:00:00
|
||||
// 02:00 local does not exist, clocks skip to 03:00
|
||||
vi.setSystemTime('2025-03-30T01:00:00Z');
|
||||
|
||||
const result = timeCore.toTimeOfDay(timeCore.now());
|
||||
expect(millisToString(result)).toBe('03:00:00');
|
||||
});
|
||||
|
||||
it('produces CEST time just before fall back', () => {
|
||||
// 2025-10-26 at 03:00 CEST clocks fall back to 02:00 CET
|
||||
// UTC 00:59:59 → Copenhagen CEST (UTC+2) → local 02:59:59
|
||||
vi.setSystemTime('2025-10-26T00:59:59Z');
|
||||
|
||||
const result = timeCore.toTimeOfDay(timeCore.now());
|
||||
expect(millisToString(result)).toBe('02:59:59');
|
||||
});
|
||||
|
||||
it('produces CET time just after fall back', () => {
|
||||
// UTC 01:00:00 → Copenhagen CET (UTC+1) → local 02:00:00
|
||||
vi.setSystemTime('2025-10-26T01:00:00Z');
|
||||
|
||||
const result = timeCore.toTimeOfDay(timeCore.now());
|
||||
expect(millisToString(result)).toBe('02:00:00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toInstant() converts a time of day back to an instant anchored to a reference day', () => {
|
||||
const testTimes = [
|
||||
{ time: '2025-01-15T08:30:00Z', label: 'winter morning' },
|
||||
{ time: '2025-06-15T14:00:00Z', label: 'summer afternoon' },
|
||||
{ time: '2025-01-01T00:00:00Z', label: 'midnight UTC on new years' },
|
||||
{ time: '2025-07-01T23:59:59Z', label: 'just before midnight UTC in summer' },
|
||||
{ time: '2025-03-15T12:00:00Z', label: 'noon UTC in winter' },
|
||||
{ time: '2025-09-15T12:00:00Z', label: 'noon UTC in summer' },
|
||||
];
|
||||
|
||||
test.each(testTimes)('roundtrips through toTimeofDay for $label ($time)', ({ time }) => {
|
||||
vi.setSystemTime(time);
|
||||
|
||||
const instant = timeCore.now();
|
||||
const clock = timeCore.toTimeOfDay(instant);
|
||||
expect(timeCore.toInstant(clock, instant)).toBe(instant);
|
||||
});
|
||||
|
||||
describe('roundtrips through DST transitions in Europe/Copenhagen', () => {
|
||||
it('roundtrips just before spring forward', () => {
|
||||
vi.setSystemTime('2025-03-30T00:58:18Z');
|
||||
|
||||
const instant = timeCore.now();
|
||||
const clock = timeCore.toTimeOfDay(instant);
|
||||
expect(timeCore.toInstant(clock, instant)).toBe(instant);
|
||||
});
|
||||
|
||||
it('roundtrips just after spring forward', () => {
|
||||
vi.setSystemTime('2025-03-30T01:00:00Z');
|
||||
|
||||
const instant = timeCore.now();
|
||||
const clock = timeCore.toTimeOfDay(instant);
|
||||
expect(timeCore.toInstant(clock, instant)).toBe(instant);
|
||||
});
|
||||
|
||||
it('roundtrips just before fall back', () => {
|
||||
vi.setSystemTime('2025-10-26T00:59:59Z');
|
||||
|
||||
const instant = timeCore.now();
|
||||
const clock = timeCore.toTimeOfDay(instant);
|
||||
expect(timeCore.toInstant(clock, instant)).toBe(instant);
|
||||
});
|
||||
|
||||
it('roundtrips just after fall back', () => {
|
||||
vi.setSystemTime('2025-10-26T01:00:00Z');
|
||||
|
||||
const instant = timeCore.now();
|
||||
const clock = timeCore.toTimeOfDay(instant);
|
||||
expect(timeCore.toInstant(clock, instant)).toBe(instant);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeSince() returns the duration elapsed since a past point', () => {
|
||||
it('measures elapsed time between two instants', () => {
|
||||
const start = 1000 as Instant;
|
||||
const end = 5000 as Instant;
|
||||
expect(timeCore.timeSince(end, start)).toBe(4000);
|
||||
});
|
||||
|
||||
it('returns negative when the reference is in the future', () => {
|
||||
const start = 5000 as Instant;
|
||||
const end = 1000 as Instant;
|
||||
expect(timeCore.timeSince(end, start)).toBe(-4000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeUntil() returns the duration until a future point', () => {
|
||||
it('measures time remaining until a future instant', () => {
|
||||
const current = 1000 as Instant;
|
||||
const target = 5000 as Instant;
|
||||
expect(timeCore.timeUntil(current, target)).toBe(4000);
|
||||
});
|
||||
|
||||
it('returns negative when the target is in the past', () => {
|
||||
const current = 5000 as Instant;
|
||||
const target = 1000 as Instant;
|
||||
expect(timeCore.timeUntil(current, target)).toBe(-4000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addDuration() moves a point in time by a duration', () => {
|
||||
it('moves an instant forward', () => {
|
||||
const instant = 1000 as Instant;
|
||||
const duration = 500 as Duration;
|
||||
expect(timeCore.addDuration(instant, duration)).toBe(1500);
|
||||
});
|
||||
|
||||
it('moves backward with a negative duration', () => {
|
||||
const instant = 1000 as Instant;
|
||||
const duration = -300 as Duration;
|
||||
expect(timeCore.addDuration(instant, duration)).toBe(700);
|
||||
});
|
||||
|
||||
it('moves by the sum of multiple durations', () => {
|
||||
const instant = 1000 as Instant;
|
||||
const durations = [500, -300, 50] as Duration[];
|
||||
expect(timeCore.addDuration(instant, durations)).toBe(1250);
|
||||
});
|
||||
|
||||
it('keeps the instant unchanged with an empty duration list', () => {
|
||||
const instant = 1000 as Instant;
|
||||
expect(timeCore.addDuration(instant, [])).toBe(1000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Instant, TimeOfDay, Duration } from 'ontime-types';
|
||||
import { dayInMs, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
/** Returns the current instant */
|
||||
export function now(): Instant {
|
||||
return Date.now() as Instant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an instant to milliseconds since midnight in the local timezone
|
||||
* - Accounts for the system's timezone offset including DST
|
||||
* - Result is always in the range [0, dayInMs)
|
||||
*/
|
||||
export function toTimeOfDay(instant: Instant): TimeOfDay {
|
||||
const tzOffset = new Date(instant).getTimezoneOffset() * MILLIS_PER_MINUTE;
|
||||
return ((((instant - tzOffset) % dayInMs) + dayInMs) % dayInMs) as TimeOfDay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current time of day in milliseconds since midnight
|
||||
*/
|
||||
export function timeOfDayNow(): TimeOfDay {
|
||||
return toTimeOfDay(now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a time of day to an instant anchored to the same day as the reference
|
||||
* - Uses the reference instant to determine which calendar day to anchor to
|
||||
*/
|
||||
export function toInstant(clock: TimeOfDay, reference: Instant): Instant {
|
||||
const referenceClock = toTimeOfDay(reference);
|
||||
const dayStart = reference - referenceClock;
|
||||
return (dayStart + clock) as Instant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration elapsed since a past instant
|
||||
* Result is positive when 'since' is before 'now'
|
||||
*/
|
||||
export function timeSince(now: Instant, since: Instant): Duration {
|
||||
return (now - since) as Duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration remaining until a future instant
|
||||
* Result is positive when 'until' is after 'now'
|
||||
*/
|
||||
export function timeUntil(now: Instant, until: Instant): Duration {
|
||||
return (until - now) as Duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an instant forward or backward by a duration
|
||||
* Use a negative duration to move backward
|
||||
*/
|
||||
export function addDuration(instant: Instant, duration: Duration | Duration[]): Instant {
|
||||
const totalDuration = Array.isArray(duration) ? duration.reduce((total, current) => total + current, 0) : duration;
|
||||
|
||||
return (instant + totalDuration) as Instant;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Playback, MaybeString, MaybeNumber } from 'ontime-types';
|
||||
import { Playback, MaybeString, MaybeNumber, Maybe, Instant } from 'ontime-types';
|
||||
|
||||
export type RestorePoint = {
|
||||
playback: Playback;
|
||||
@@ -7,6 +7,6 @@ export type RestorePoint = {
|
||||
addedTime: number;
|
||||
pausedAt: MaybeNumber;
|
||||
firstStart: MaybeNumber;
|
||||
startEpoch: MaybeNumber;
|
||||
startEpoch: Maybe<Instant>;
|
||||
currentDay: MaybeNumber;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TimerPhase, Playback, OffsetMode } from 'ontime-types';
|
||||
import { TimerPhase, Playback, OffsetMode, type TimeOfDay } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
import type { RuntimeState } from '../runtimeState.js';
|
||||
|
||||
const baseState: RuntimeState = {
|
||||
clock: 0,
|
||||
clock: 0 as TimeOfDay,
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
eventFlag: null,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
isOntimeEvent,
|
||||
Instant,
|
||||
MaybeNumber,
|
||||
MaybeString,
|
||||
OffsetMode,
|
||||
@@ -10,9 +11,11 @@ import {
|
||||
Rundown,
|
||||
Offset,
|
||||
runtimeStorePlaceholder,
|
||||
TimeOfDay,
|
||||
TimerPhase,
|
||||
TimerState,
|
||||
RundownState,
|
||||
Maybe,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
calculateDuration,
|
||||
@@ -23,7 +26,6 @@ import {
|
||||
isPlaybackActive,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { getTimeObject, timeNow } from '../utils/time.js';
|
||||
import type { RestorePoint } from '../services/restore-service/restore.type.js';
|
||||
import {
|
||||
findDayOffset,
|
||||
@@ -36,11 +38,12 @@ import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
||||
import { timerConfig } from '../setup/config.js';
|
||||
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
||||
import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js';
|
||||
import * as timeCore from '../lib/time-core/timeCore.js';
|
||||
|
||||
type ExpectedMetadata = { event: OntimeEvent; accumulatedGap: number; isLinkedToLoaded: boolean } | null;
|
||||
|
||||
export type RuntimeState = {
|
||||
clock: number; // realtime clock
|
||||
clock: TimeOfDay;
|
||||
groupNow: OntimeGroup | null;
|
||||
eventNow: PlayableEvent | null;
|
||||
eventNext: PlayableEvent | null;
|
||||
@@ -50,9 +53,9 @@ export type RuntimeState = {
|
||||
rundown: RundownState;
|
||||
// private properties of the timer calculations
|
||||
_timer: {
|
||||
forceFinish: MaybeNumber; // whether we should declare an event as finished, will contain the finish time
|
||||
pausedAt: MaybeNumber;
|
||||
secondaryTarget: MaybeNumber;
|
||||
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
|
||||
pausedAt: Maybe<TimeOfDay>;
|
||||
secondaryTarget: Maybe<TimeOfDay>;
|
||||
hasFinished: boolean;
|
||||
};
|
||||
_rundown: {
|
||||
@@ -61,12 +64,12 @@ export type RuntimeState = {
|
||||
_group: ExpectedMetadata;
|
||||
_flag: ExpectedMetadata;
|
||||
_end: ExpectedMetadata;
|
||||
_startEpoch: MaybeNumber;
|
||||
_startEpoch: Maybe<Instant>;
|
||||
_startDayOffset: MaybeNumber;
|
||||
};
|
||||
|
||||
const runtimeState: RuntimeState = {
|
||||
clock: timeNow(),
|
||||
clock: timeCore.timeOfDayNow(),
|
||||
groupNow: null,
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
@@ -122,7 +125,7 @@ export function clearEventData() {
|
||||
runtimeState.rundown.selectedEventIndex = null;
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = timeNow();
|
||||
runtimeState.clock = timeCore.timeOfDayNow();
|
||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||
|
||||
// when clearing, we maintain the total delay from the rundown
|
||||
@@ -154,7 +157,7 @@ export function clearState() {
|
||||
runtimeState._end = null;
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = timeNow();
|
||||
runtimeState.clock = timeCore.timeOfDayNow();
|
||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||
|
||||
// when clearing, we maintain the total delay from the rundown
|
||||
@@ -354,10 +357,13 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
|
||||
if (runtimeState._timer.secondaryTarget !== null) {
|
||||
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
|
||||
// if the event is now, we queue a start
|
||||
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart;
|
||||
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay;
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||
} else {
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||
runtimeState.eventNow.timeStart,
|
||||
offsetClock,
|
||||
) as TimeOfDay;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,7 +409,9 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [epoch, now] = getTimeObject();
|
||||
const epoch = timeCore.now();
|
||||
const now = timeCore.toTimeOfDay(epoch);
|
||||
|
||||
state.clock = now;
|
||||
state.timer.secondaryTimer = null;
|
||||
|
||||
@@ -459,7 +467,7 @@ export function pause(state: RuntimeState = runtimeState): boolean {
|
||||
}
|
||||
|
||||
state.timer.playback = Playback.Pause;
|
||||
state.clock = timeNow();
|
||||
state.clock = timeCore.timeOfDayNow();
|
||||
state._timer.pausedAt = state.clock;
|
||||
return true;
|
||||
}
|
||||
@@ -494,7 +502,7 @@ export function addTime(amount: number) {
|
||||
|
||||
if (willGoNegative && !runtimeState._timer.hasFinished) {
|
||||
// set finished time so side effects are triggered
|
||||
runtimeState._timer.forceFinish = timeNow();
|
||||
runtimeState._timer.forceFinish = timeCore.timeOfDayNow();
|
||||
} else {
|
||||
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
||||
if (willGoPositive) {
|
||||
@@ -524,7 +532,8 @@ export type UpdateResult = {
|
||||
export function update(): UpdateResult {
|
||||
// 0. there are some things we always do
|
||||
const previousClock = runtimeState.clock;
|
||||
const [epoch, now] = getTimeObject();
|
||||
const epoch = timeCore.now();
|
||||
const now = timeCore.toTimeOfDay(epoch);
|
||||
runtimeState.clock = now; // we update the clock on every update call
|
||||
|
||||
// 1. is playback idle?
|
||||
@@ -600,10 +609,13 @@ export function update(): UpdateResult {
|
||||
if (hasCrossedMidnight) {
|
||||
// if we crossed midnight, we need to update the target
|
||||
// this is the same logic from the roll function
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||
runtimeState.eventNow.timeStart,
|
||||
offsetClock,
|
||||
) as TimeOfDay;
|
||||
}
|
||||
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget! - offsetClock;
|
||||
return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0 };
|
||||
}
|
||||
}
|
||||
@@ -620,7 +632,8 @@ export function roll(
|
||||
}
|
||||
|
||||
// we will need to do some calculations, update the time first
|
||||
const [epoch, now] = getTimeObject();
|
||||
const epoch = timeCore.now();
|
||||
const now = timeCore.toTimeOfDay(epoch);
|
||||
runtimeState.clock = now;
|
||||
|
||||
// 2. if there is an event armed, we use it
|
||||
@@ -674,7 +687,10 @@ export function roll(
|
||||
runtimeState._startEpoch = epoch;
|
||||
}
|
||||
} else {
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||
runtimeState.eventNow.timeStart,
|
||||
offsetClock,
|
||||
) as TimeOfDay;
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
}
|
||||
@@ -720,7 +736,7 @@ export function roll(
|
||||
// there is nothing now, but something coming up
|
||||
runtimeState.timer.phase = TimerPhase.Pending;
|
||||
// we need to normalise start time in case it is the day after
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
|
||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay;
|
||||
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||
|
||||
// preload timer properties
|
||||
|
||||
@@ -74,18 +74,3 @@ export function timeNow() {
|
||||
elapsed += now.getMilliseconds();
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time from system
|
||||
* @returns [number, number] - [epoch time, milliseconds since midnight]
|
||||
*/
|
||||
export function getTimeObject(): [number, number] {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
return [now.getTime(), elapsed];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "../../tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"composite": true,
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"noImplicitReturns": false, //TODO: fix this
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config';
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
globalSetup: './vitest.global-setup.ts',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function setup() {
|
||||
process.env.TZ = 'Europe/Copenhagen';
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
declare const __brand: unique symbol;
|
||||
type Brand<B> = { [__brand]: B };
|
||||
type Branded<T, B> = T & Brand<B>;
|
||||
|
||||
/** A timestamp in milliseconds from epoch */
|
||||
export type Instant = Branded<number, 'instant'>;
|
||||
|
||||
/** A timestamp of milliseconds since midnight today in the set timezone */
|
||||
export type TimeOfDay = Branded<number, 'time-of-day'>;
|
||||
|
||||
/** A duration of milliseconds */
|
||||
export type Duration = Branded<number, 'duration'>;
|
||||
|
||||
/** A day count integer */
|
||||
export type Day = Branded<number, 'day'>;
|
||||
@@ -20,6 +20,9 @@ export type { RundownEntries, Rundown, ProjectRundowns } from './definitions/cor
|
||||
export { TimeStrategy } from './definitions/TimeStrategy.type.js';
|
||||
export { TimerType } from './definitions/TimerType.type.js';
|
||||
|
||||
// ---> Core
|
||||
export type { Day, Duration, Instant, TimeOfDay } from './definitions/core/Temporal.js';
|
||||
|
||||
// ---> Report
|
||||
export type { OntimeReport, OntimeEventReport } from './definitions/core/Report.type.js';
|
||||
|
||||
@@ -125,7 +128,7 @@ export {
|
||||
isOntimeAction,
|
||||
isTimerLifeCycle,
|
||||
} from './utils/guards.js';
|
||||
export type { MaybeNumber, MaybeString } from './utils/utils.type.js';
|
||||
export type { Maybe, MaybeNumber, MaybeString } from './utils/utils.type.js';
|
||||
|
||||
// Colour
|
||||
export type { RGBColour } from './definitions/Colour.type.js';
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export type MaybeNumber = number | null;
|
||||
export type MaybeString = string | null;
|
||||
|
||||
export type Maybe<T> = T | null;
|
||||
|
||||
Reference in New Issue
Block a user