Primitives / Time
debounce and Throttle
debounce({ name, value }) is a factory over any value Schema. Changed
restarts a delayMs timer, and only the latest value settles, as an
OutMessage the placement must handle with onOut, so a settled search query
can never be dropped by omission:
import type * as Update from 'foldkit/update'
import { debounce } from 'foldkit-primitives/time'
const Query = debounce({ name: 'Query', value: Schema.String })
const Base = Bundle.compose({ results: Schema.Array(Schema.String) }).pipe(
Bundle.withMessages({ Found: { results: Schema.Array(Schema.String) } }),
Bundle.withChild('query', Query),
)
type SearchModel = typeof Base.Model.Type
type SearchMessage = typeof Base.Message.Type
const fire =
({ value }: { readonly value: string }): Update.Step<SearchModel, SearchMessage> =>
model => ({ model, commands: [search(value)] })
const Search = Base.pipe(Bundle.configure('query', { args: { delayMs: 300 }, onOut: fire }))The onOut returns a Command whose Message is the parent's own Found, so
it is written against Base.Model and Base.Message and given through
Bundle.configure, as the Bundle README
describes for a config made from the parent. An onOut that only writes the
Model goes straight in withChild's config.
Each Changed bumps a generation the scheduled Settled carries; a superseded
timer's fact arrives with an old generation and emits nothing.
Throttle is the leading edge to debounce's trailing one: the first
Attempted in an intervalMs window surfaces Throttled { at } (through
onOut, likewise required) and the rest are dropped. Attempted reads the
clock through a Command, so the check runs on Effect time and tests drive it;
the window boundary counts as past. Pair the two rather than adding a second
timer.
Relative time
formatRelativeTime(from, to, locale?) picks the unit, seconds through years,
and lets Intl.RelativeTimeFormat word it, so locales come from the platform
rather than a phrase table. There is deliberately no now helper:
Clock.currentTimeMillis already is it.
Failure
A non-positive or non-finite interval is rejected at placement. While stopped, a tick stream is empty. A superseded debounce timer emits nothing.