@faybl/utils
Generic JavaScript for Faybl services and web apps.
npm install @faybl/utils
History
fetchHistory reads a channel's history through a faybl-client as an array in
ascending time, each point with ts and pl, which is what a chart draws.
import { fetchHistory, fetchHistoryLast } from '@faybl/utils';
const { data } = await fetchHistory({ client, rid, from, to });
const newest = await fetchHistoryLast({ client, rid });
It decides ascending and reverse for data-history from the range:
| Asked for | Returns |
|---|---|
from, with or without to | the range, from its start |
limit and from | the first limit points from from |
limit without from | the newest limit points, before to when given |
| nothing | from the oldest point |
samplingInterval, downsamplingMethod and timeZone are passed on. Empty
values are left out, and nothing else reaches data-history.
fetchHistoryLast is the newest point, before to when given, or undefined.
Waiting
waitFor calls a function until its promise resolves, and resolves with the
value. It is how a service waits at start for a dependency that comes up on its
own, logging each attempt instead of exiting.
import { waitFor } from '@faybl/utils';
const storage = await waitFor(connectToDatabase, {
onRetry: (error, attempt) => logger.warn(`Database not reachable, attempt ${attempt}:`, error),
});
| Option | Default | What it does |
|---|---|---|
interval | 5000 | Milliseconds between attempts |
retryIf | always true | Given the rejection; false passes it on instead of trying again |
onRetry | does nothing | Given the rejection and the attempt's number, before the wait |
There is no limit on attempts.
delay(milliseconds, signal?) resolves after the time. An abort of signal
clears the timer and rejects with the signal's reason.
await delay(1000);
await delay(1000, AbortSignal.timeout(500)); // rejects after 500 ms
Paths and picks
pathStr('pl.temp')(data) reads a dot-separated path, such as one a user types;
an array index is a number in the path ('list.0'). A missing path and a null
(or NaN) value both give undefined.
pickRandom(items) is a random item, or undefined for none. It takes its
randomness as a second argument for tests: pickRandom(items, () => 0).
Clipboard
copyText(text) writes to the system clipboard and resolves to a Result: Ok,
or Err({ kind: 'ClipboardWriteFailed', cause }) when there is no clipboard,
permission is denied or the page has no focus. Call it from the click handler
before any await, or the browser rejects the write.
match(({ cause }) => console.warn('Copy by hand:', cause))(() => setCopied(true))(
await copyText(url),
);
Result
Expected failures as values: plain, serializable data, Ok(value) =
{ ok: true, value } or Err(error) = { ok: false, error }. Compose with
map / chain / mapErr from this package (never R.map / R.chain: Ramda
maps a Result's fields as a plain object), collect a list with all (first
Err wins) and unwrap once at the edge with match(onErr)(onOk).
import * as R from 'ramda';
import { Err, Ok, chain, fromNil, match, tryCatchAsync } from '@faybl/utils';
const fetchJson = tryCatchAsync(async (url) => Ok(await (await fetch(url)).json()))((cause, url) =>
Err({ kind: 'FetchFailed', url, cause }),
);
const getPort = R.pipe(R.path(['server', 'port']), fromNil({ kind: 'MissingPort' }));
const port = chain(getPort)(await fetchJson(url));
match(({ kind }) => console.error(kind))(console.log)(port);
Wrap foreign throwers at the boundary with R.tryCatch (sync) or
tryCatchAsync (async). When the wrapped call can also throw errors that are
not expected failures, use catchWhen(isExpected)(toErr) as the catcher: it
maps matching errors to an Err and rethrows anything else unchanged, so
defects still crash.
const readText = tryCatchAsync(async (path) => Ok(await readFile(path, 'utf8')))(
catchWhen((e) => e?.code === 'ENOENT')((cause, path) =>
Err({ kind: 'FileNotFound', path, cause }),
),
);
Channels
channelsOfParent(parent)(records) keeps the channel records an asset or thing
lists in its channels (name → rid), each with nameFromParent; a missing
parent gives none. recordsToSelectOptions turns channel records into
{ id: rid, label } options.
Names and mock data
haiku() is a random two-word title such as Misty creek. mockHistory() is
ten points over the last ten seconds, shaped like a data-history response, for an
app running without a backend; mockHistoryEmpty() has none. Both take their
randomness (and mockHistory its now) as arguments for tests.