@uniflowed/core/clock
type
Clock
export type Clock = {
/** Milliseconds since the Unix epoch. */
readonly now: () => number,
/** The IANA name of the zone this clock reports in, for example `Asia/Tokyo`. */
readonly timeZone: () => string,
};
A source of the current time, and of the zone it is reported in.
variable
UTC
export const UTC: string = "UTC";
The zone uf falls back to when the host cannot name its own.
UTC rather than a guess from the offset: an offset is not a zone — it does not know when the offset changes — and a wrong zone name produces text that is confidently wrong twice a year.
function
systemClock
export function systemClock(): Clock { ... }
Whatever the host says, which is what an application gets by default.
function
hostTimeZone
export function hostTimeZone(): string { ... }
The host's own zone, or UTC where it has none.
Read on every call rather than once at import: a Node process can be started with TZ unset and have it set later, and a test that installs a zone would otherwise be fighting a value captured before it ran.
function
fixedClock
export function fixedClock(epochMilliseconds: number, timeZone: string = UTC): Clock { ... }
A clock stopped at one instant.
This is what a server render and a test both want, for the same reason: the value must not change between the first read and the last, or two components on the same page disagree about what "today" is.
type
ManualClock
export type ManualClock = {
readonly clock: Clock,
/** Move forward by `millis`, which may be negative. */
readonly advance: (millis: number) => void,
/** Move to an exact instant. */
readonly set: (epochMilliseconds: number) => void,
};
A clock and the two handles that move it.
function
manualClock
export function manualClock(epochMilliseconds: number, timeZone: string = UTC): ManualClock { ... }
A clock a test drives by hand.
Separate from fixedClock because the two answer different questions. A fixed clock proves that a value does not depend on when it was read; a manual one proves that something *does* move when time does — that "in 5 minutes" becomes "in 4 minutes" — without waiting five minutes to find out.
function
currentClock
export function currentClock(): Clock { ... }
The clock every uf package reads. The host's, unless something installed one.
function
setClock
export function setClock(clock: Clock | null): () => void { ... }
Install clock, and return the call that puts back whatever was there.
The undo is returned rather than left to the caller to reconstruct, because the caller reconstructing it is how nested installs go wrong: two tests that each restore "the system clock" instead of "what I found" leave the second one's clock installed for everything that runs after them. null restores the host's.
function
now
export function now(): number { ... }
Milliseconds since the epoch, from the installed clock.
function
timeZone
export function timeZone(): string { ... }
The zone the installed clock reports in.
@uniflowed/core/native
type
ModuleSpecifier
export type ModuleSpecifier = string;
Specifier of the shipped subpath a binding belongs to, for example @uniflowed/core/effect.
type
NativeHandle
export type NativeHandle<Name extends string> = { readonly __ufNative: Name };
Phantom carrier for an opaque handle the native runtime owns.
Name is a distinct string literal per handle, which keeps two unrelated handles from unifying even inside the module that defines them.
type
NativeHandleInvariant
export type NativeHandleInvariant<Name extends string, T> = {
readonly __ufNative: Name,
__ufValue: T,
};
Phantom carrier for an opaque handle with an invariant type parameter.
T sits behind a writable property, which is precisely what invariance means: Handle<Dog> is neither a subtype nor a supertype of Handle<Animal>.
type
NativeHandleCovariant
export type NativeHandleCovariant<Name extends string, out T> = {
readonly __ufNative: Name,
readonly __ufValue: () => T,
};
Phantom carrier for an opaque handle with a covariant type parameter.
T only ever appears in a return position, so Handle<Dog> stays assignable to Handle<Animal> — the guarantee a +T sigil makes to callers.
type
NativeHandleCovariant2
export type NativeHandleCovariant2<Name extends string, out A, out B> = {
readonly __ufNative: Name,
readonly __ufFirst: () => A,
readonly __ufSecond: () => B,
};
Phantom carrier for a handle with two covariant type parameters.
Same guarantee as NativeHandleCovariant, for a handle that tracks two things at once — a fiber's success and failure types, say.
type
NativeHandleCovariant3
export type NativeHandleCovariant3<Name extends string, out A, out B, out C> = {
readonly __ufNative: Name,
readonly __ufFirst: () => A,
readonly __ufSecond: () => B,
readonly __ufThird: () => C,
};
Phantom carrier for a handle with three covariant type parameters.
An effect tracks what it produces, how it fails, and what it needs; each sits behind a function that returns it, which is what makes all three covariant rather than merely declared so.
class
NativeRuntimeRequiredError
export class NativeRuntimeRequiredError extends Error { ... }
Raised when a @uniflowed/* binding is reached outside the uf native runtime.
The message names both the subpath and the binding, so a caller sees @uniflowed/core/effect: effect() requires the uf native runtime rather than a generic failure.
function
nativeRuntimeRequired
export function nativeRuntimeRequired(moduleSpecifier: ModuleSpecifier, binding: string): empty { ... }
Raise NativeRuntimeRequiredError for one binding.
Returns empty, Flow's bottom type, so a call site can return it from a function of any declared return type without weakening that type to any or mixed. Every shipped binding raises on *call*; importing a subpath stays free of side effects so bundlers can drop the modules an application never touches.
@uniflowed/core/random
type
Random
export type Random = {
/** The next number in `[0, 1)`. */
readonly next: () => number,
/** The next integer in `[0, bound)`. Zero when `bound` is not positive. */
readonly integer: (bound: number) => number,
/**
* An independent stream named by `label`.
*
* Independent of *when* it is asked for, which is the property the module
* documentation is about: forking twice with the same label yields two
* streams that produce the same numbers.
*/
readonly fork: (label: string) => Random,
};
A stream of numbers that any host can replay from the same seed.
function
seededRandom
export function seededRandom(seed: string | number): Random { ... }
A stream that replays exactly, on every host, from seed.
A number seed is stringified rather than used as state directly, so seededRandom(7) and seededRandom("7") are the same stream. They arrive at the same place through JSON anyway — one round trip through the markup turns a number into a number and a string into a string, and a caller who wrote one and read the other should not get two different pages.
function
hostSeed
export function hostSeed(): string { ... }
A seed from the host, for the one render that has to decide.
Base 36 and eight characters: short enough to sit in the markup without being noticed, and wide enough that two pages rendered in the same millisecond do not collide. Deliberately Math.random() — this is the one call site where non-determinism is the requirement rather than the bug, and hiding it behind crypto would suggest the value is a secret.
function
currentRandom
export function currentRandom(): Random { ... }
The stream every uf package reads.
Unseeded until something installs one, and unseeded means seeded from the host: a page nobody prerendered has no other side to agree with, and refusing to produce a number until a seed is installed would make the common case the one that needs configuring.
function
setRandom
export function setRandom(random: Random | null): () => void { ... }
Install random, and return the call that puts back whatever was there.
The same contract as setClock, and for the same reason: a caller that restores what it assumed was there rather than what it found leaves its own stream installed for everything that runs afterwards.
function
shuffled
export function shuffled<T>(items: $ReadOnlyArray<T>, random: Random): Array<T> { ... }
items, in an order that depends only on random.
Fisher-Yates over a copy, because a shuffle that mutates its argument is a shuffle that changes a prop, and React compares props. The draw is taken before the swap on every step, so the number of draws is a function of the length alone — a stream is only replayable if both sides take the same number of numbers out of it.
@uniflowed/core/temporal
interface
Instant
export interface Instant {
readonly epochMilliseconds: number;
toZonedDateTimeISO(timeZone: string): ZonedDateTime;
add(duration: Duration | DurationLike | string): Instant;
subtract(duration: Duration | DurationLike | string): Instant;
since(other: Instant): Duration;
until(other: Instant): Duration;
equals(other: Instant): boolean;
toString(): string;
toJSON(): string;
}
A point in time, with no zone and no calendar.
interface
ZonedDateTime
export interface ZonedDateTime {
readonly epochMilliseconds: number;
readonly timeZoneId: string;
readonly year: number;
readonly month: number;
readonly day: number;
readonly hour: number;
readonly minute: number;
readonly second: number;
readonly millisecond: number;
readonly dayOfWeek: number;
readonly offset: string;
toInstant(): Instant;
toPlainDate(): PlainDate;
toPlainTime(): PlainTime;
toString(): string;
toJSON(): string;
toLocaleString(locales?: string, options?: DateTimeFormatOptions): string;
equals(other: ZonedDateTime): boolean;
}
A date and a time in a named zone: what a reader actually sees on a clock.
interface
PlainDate
export interface PlainDate {
readonly year: number;
readonly month: number;
readonly day: number;
/** ISO 8601: 1 is Monday and 7 is Sunday, on every host and in every locale. */
readonly dayOfWeek: number;
/** 28, 29, 30 or 31, for the month this date is in. */
readonly daysInMonth: number;
add(duration: Duration | DurationLike | string): PlainDate;
subtract(duration: Duration | DurationLike | string): PlainDate;
equals(other: PlainDate): boolean;
toString(): string;
toJSON(): string;
toLocaleString(locales?: string, options?: DateTimeFormatOptions): string;
}
A calendar date that never had a time to lose.
interface
PlainTime
export interface PlainTime {
readonly hour: number;
readonly minute: number;
readonly second: number;
readonly millisecond: number;
equals(other: PlainTime): boolean;
toString(): string;
toJSON(): string;
}
A wall-clock time with no date attached.
interface
Duration
export interface Duration {
readonly years: number;
readonly months: number;
readonly weeks: number;
readonly days: number;
readonly hours: number;
readonly minutes: number;
readonly seconds: number;
readonly milliseconds: number;
readonly blank: boolean;
negated(): Duration;
abs(): Duration;
total(options: { readonly unit: string, ... }): number;
toString(): string;
toJSON(): string;
}
A length of time, in the units it was written in.
type
DurationLike
export type DurationLike = {
readonly years?: number,
readonly months?: number,
readonly weeks?: number,
readonly days?: number,
readonly hours?: number,
readonly minutes?: number,
readonly seconds?: number,
readonly milliseconds?: number,
};
What a Duration can be written as at a call site.
type
export type DateTimeFormatOptions = {
readonly timeZone?: string,
readonly timeZoneName?: "short" | "long" | "shortOffset" | "longOffset",
readonly calendar?: string,
readonly dateStyle?: "full" | "long" | "medium" | "short",
readonly timeStyle?: "full" | "long" | "medium" | "short",
readonly weekday?: "narrow" | "short" | "long",
readonly era?: "narrow" | "short" | "long",
readonly year?: "numeric" | "2-digit",
readonly month?: "numeric" | "2-digit" | "narrow" | "short" | "long",
readonly day?: "numeric" | "2-digit",
readonly hour?: "numeric" | "2-digit",
readonly minute?: "numeric" | "2-digit",
readonly second?: "numeric" | "2-digit",
readonly hour12?: boolean,
readonly hourCycle?: "h11" | "h12" | "h23" | "h24",
...
};
The Intl.DateTimeFormat options this module passes and forwards.
Flow's vendored intl.js declares no Intl$DateTimeFormatOptions at all, so the name an editor suggests does not resolve. Declared here as the keys that are used, plus the ones a caller of toLocaleString reasonably passes on.
Every property is readonly, which is not decoration. An optional property that is writable is invariant, so an object built by spreading a caller's options would be rejected for having fewer keys than the type names — and the only way to satisfy that is to write all fourteen at every call site.
type
TemporalNow
export type TemporalNow = {
instant(): Instant,
timeZoneId(): string,
zonedDateTimeISO(timeZone?: string): ZonedDateTime,
plainDateISO(timeZone?: string): PlainDate,
plainTimeISO(timeZone?: string): PlainTime,
};
The clock, and what is read from it.
type
TemporalTypes
export type TemporalTypes = {
readonly Instant: {
from(value: Instant | string): Instant,
fromEpochMilliseconds(epochMilliseconds: number): Instant,
compare(a: Instant, b: Instant): number,
...
},
readonly ZonedDateTime: {
from(value: ZonedDateTime | string): ZonedDateTime,
compare(a: ZonedDateTime, b: ZonedDateTime): number,
...
},
readonly PlainDate: {
from(value: PlainDate | string | { year: number, month: number, day: number, ... }): PlainDate,
compare(a: PlainDate, b: PlainDate): number,
...
},
readonly PlainTime: { from(value: PlainTime | string): PlainTime, ... },
readonly Duration: { from(value: Duration | DurationLike | string): Duration, ... },
...
};
The five constructors, without the clock hung off them.
type
TemporalApi
export type TemporalApi = { ...TemporalTypes, readonly Now: TemporalNow, ... };
The surface uf promises on every host. Native Temporal is a superset of it.
variable
isLite
export const isLite: boolean = host == null;
Whether the five types come from this module rather than from the host.
variable
Temporal
export const Temporal: TemporalApi = (() => {
const types: TemporalTypes = host ?? lite;
const named = {
Instant: types.Instant,
ZonedDateTime: types.ZonedDateTime,
PlainDate: types.PlainDate,
PlainTime: types.PlainTime,
Duration: types.Duration,
Now: nowFor(types),
};
if (host == null) {
return named;
}
return {
...named,
PlainDateTime: host.PlainDateTime,
PlainYearMonth: host.PlainYearMonth,
PlainMonthDay: host.PlainMonthDay,
};
})();
Temporal, on every host.
Every constructor is named rather than spread from the host, and that is the fix for #1008 rather than a matter of taste. A spread copies own *enumerable* properties, and the constructors on a built-in namespace are not enumerable — { ...Math } is {} for the same reason — so on Node 26, which ships a native Temporal, the export spread from it was { Now } and Temporal.Instant was undefined. The Lite object is a literal, which is why no host without Temporal could see it, CI's Node 24 included.
The three constructors Lite leaves out are named too, so that what this module has not documented — PlainDateTime and the two beside it — stays reachable on a host that has it rather than being hidden by uf. They are added only when the host is the one in use, so a Lite Temporal is still the five and Now, key for key. named is spread to do that, which is safe for the reason the host was not: it is a literal written two lines above. Now is replaced in both cases, for the reason at the top of this file.
Without a doc comment