Shared utilities: FP helpers, formatting, slugs, caching, and logging.
Functional Programming
Curried utilities for data transformation:
pipe, filter, map, reduce, compact, unique, and more.
Formatting
Currency formatting, phone normalization, markdown rendering, and timezone-aware date/time display.
Caching
TTL and LRU caches with a global registry for admin stats.
Queue a promise that must complete before the response is sent
Narrow an unknown value to string, defaulting to "" if not a string.
Replaces typeof x === "string" ? x : "" at type boundaries.
Resource management pattern (like Haskell's bracket or try-with-resources). Ensures cleanup happens even if the operation throws.
Key items by their own id, so a caller can look one up. Hand-rolled because
@std/collections.associateBy keys by a string and gives back an object,
while every caller here has number ids and wants a Map.
Split an array into chunks of a given size.
Curried adapter over @std/collections.chunk (which throws for size < 1).
Create an in-memory collection cache with TTL. Loads all items via fetchAll on first access or after invalidation/expiry, then serves from memory until the TTL expires or invalidate() is called. Accepts an optional clock function for testing.
Create a request timer for measuring duration
The epoch-ms instant of the START of a calendar day (00:00 local time) in the
given timezone. Used to turn a YYYY-MM-DD filter bound into the integer
occurred_at bound the ledger queries compare against, so a day range is
interpreted in the operator's own timezone rather than UTC.
Resolve after ms milliseconds — for retry backoff and similar waits.
A Map holding an empty list for each key, ready to be filled.
The YYYY-MM-DD calendar day an epoch-ms instant falls on in tz. The
inverse direction of dayStartEpochMs, for labelling a stored
occurred_at as the local day it belongs to.
Epoch seconds maxAgeSeconds from now — the expiry (e) that signed tokens
carry, kept in one place so every builder computes it the same way.
A copy of base with extra entries merged on top (an extra key wins over
the same key in base). Curried so a constant overlay (a fixed content type,
a default set) can extend whatever record it is given.
Index one field from each item by its id.
Curried filter
Run a valibot schema with abortPipeEarly and return the first error message or null.
Alternative combinator: try a sequence of producers in order and return the first that yields a defined value, or undefined if every one declines.
Check items in order and stop at the first reported problem.
Curried flatMap
Await all queued work. Call before returning the response. Repeats until the queue stays empty: work already running can queue more (a background job that fails queues its error's activity-log write), and a single pass would discard those late arrivals unawaited.
Format an amount in minor units (pence/cents) as a currency string. e.g. formatCurrency(1050) → "£10.50" (when the site currency is GBP). A stored provider amount passes its own currency so the symbol and minor-unit divisor describe that charge rather than today's site setting.
Format a UTC ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Compact format for table cells: "yyyy-MM-dd HH:mm" in the given timezone.
Delegates to the browser-compatible formatIsoForPreview helper so the
same formatting runs on the server and in the admin JS bundle.
Format the safe Sentry message and activity-log base.
Format an error detail string with request context and error message
Format a signed change in minor units. Positive value is added, negative value is removed, and zero has no misleading sign.
Generate a random slug with at least 2 digits and 2 letters. Uses Fisher-Yates shuffle on the fixed positions to avoid bias.
Generate a unique slug by retrying random slugs until one is not taken.
Collect stats from all registered caches
Get the number of decimal places for a currency code
Get the current request ID, or empty string if outside request context
Group rows by a key, keeping only the chosen value from each row. Keys appear in first-occurrence order and each value list preserves input order.
True when running inside a runWithPendingWork scope (i.e. a request).
Return a value unchanged.
Fire every cache invalidator registered against table (no-op if none).
The write narrows nothing, so column-gated entries fire too.
Fire registered cache invalidators for table, respecting column gates.
Remove null and undefined values from array
ISO timestamp a fixed duration after the current time — for a column that says when to do something next, rather than when something happened.
ISO timestamp a fixed duration before the current time.
Ask whether a value is one of a fixed list: isOneOf(["a", "b"])("a") is
true. Curried, so a list of words becomes one named check.
True when a link/image URL is safe to render. Relative URLs (no scheme) are
allowed; absolute URLs must use a scheme from SAFE_URL_SCHEMES.
Leading ASCII control characters and spaces — which browsers strip before
resolving a scheme — are removed first so java\tscript: can't sneak through.
True when text is markdown so simple it renders as nothing more than a
single <p> of plain text — no bold, italic, links, lists, headings, code,
blockquotes, tables, or multiple paragraphs. When this returns true the
question can safely be used as the clickable label of its control; when
false the question should be rendered as a prose block above the control.
Check if a naive datetime-local string is a parseable datetime. Does not interpret timezone — purely a format check.
Validate that a string is a valid IANA timezone identifier.
Keep the items a test accepts, then take one thing from each.
Resettable lazy reference - like once() but can be reset for testing. Returns [get, set] tuple where set(null) resets to uncomputed state.
Convert a naive datetime-local value (YYYY-MM-DDTHH:MM) to a UTC ISO string, interpreting the value as local time in the given timezone.
Log a failed database operation under the standard DB_QUERY code.
Log a debug message with category prefix For detailed debugging during development
Log a classified error to console.error and persist to the activity log. Console output uses error codes and safe metadata (never PII). Activity log entry is encrypted and visible to admins on the log pages.
Log a classified error to console.error only (no ntfy, no activity log). Use this where calling logError would cause infinite recursion (e.g. ntfy.ts).
Log a completed request to console.debug Path is automatically redacted for privacy
Curried map
Index items by one field and chosen value. Keys keep first-occurrence order, while later matching items replace the stored value.
Index items by id and a chosen value.
Curried map that drops null/undefined results in one pass.
Curried adapter over @std/collections.mapNotNullish.
Replaces the two-step pattern: compact(map(fn)(array))
Map over a promise-returning function in parallel (Promise.all)
Strip non-numeric characters from a phone number and normalize to +{prefix}{local}
Normalize a user-provided slug: trim, lowercase, replace spaces with hyphens
Current time as a Date
Full ISO-8601 timestamp for created/logged_at fields
Epoch milliseconds for numeric comparisons
Current time in whole epoch seconds — the unit signed-token expiry uses.
Lazy evaluation - compute once on first call, cache forever.
Use instead of let x = null; const getX = () => x ??= compute();
The epoch milliseconds a written date names, or null when it names none —
the safe wrapper around Date.parse, whose own failure mode (NaN) is easy
to let leak into arithmetic by accident.
Split an array into [matching, rest] by a predicate, keeping order.
Curried adapter over @std/collections.partition.
Build tel: and wa.me hrefs for a phone number, or null when the number
has no digits. The prefix is the country dialling code (e.g. "44"); a
leading "+" is tolerated so a settings value of either "44" or "+44" works.
WhatsApp's wa.me wants the international number with no leading "+".
Compose functions left-to-right (pipe).
Whole numbers counting up from start, stopping before end ([] when end
is not past start). Hand-rolled because @std/collections has no numeric
range helper. Iterating this bounded array replaces hand-counted loops,
which a single slip in their step can make endless.
Curried reduce
Register invalidate to fire whenever ownTable or any deps entry is
written. Plain string entries are unconditional; object entries may carry
whenColumns to gate on specific UPDATE columns (INSERT/DELETE always fire).
Centralises the registration loop shared by cachedTable and cachedEntityTable.
Register invalidate to run whenever any of tables is written.
Renders block markdown, escaping raw HTML and stripping unsafe URLs.
Read a required map entry, failing where a broken completeness invariant is first observed instead of passing an undefined value onward.
Clear every registered cache: each table-registered invalidator once (a cache registered against several tables still only clears once) plus every extra reset hook. Runs after operations that bypass the normal write path — a full reset or a restore — where any warm cache is stale. A lazily-loaded cache module that never ran is absent from the registry, which is correct: it has no cache to clear.
Whether two records hold the same value in every named field.
Whether two sequences hold the same values, in the same order. It stops at the first difference, so how long it takes says how much matched: never compare secrets with it.
Set module-level request log suppression (avoids env race in parallel tests).
Turn arbitrary text into a URL slug: lowercase, every run of non
[a-z0-9] collapsed to a single hyphen, no leading/trailing hyphen. Shared
by the news permalink builder and the provider-resource slug.
Non-mutating sort with comparator
The numbers, each once, smallest first.
Sort strings in ascending locale order without changing the input.
Curried group-and-sum: accumulate valueOf(item) into a Map keyed by
keyOf(item). Replaces the common pattern:
const m = new Map(); for (const x of xs) m.set(k(x), (m.get(k(x)) ?? 0) + v(x))
Curried sum-by-selector. Adds up the numbers produced by selector for each
item. Curried adapter over @std/collections.sumOf.
Replaces the common pattern: reduce((acc, x) => acc + selector(x), 0)
Get today's date as YYYY-MM-DD in the given timezone.
Convert minor units to major units string for form display. e.g. toMajorUnits(1050) → "10.50" (for GBP)
Convert major units (decimal) to minor units (integer).
e.g. toMinorUnits(10.50) → 1050 (for GBP)
currency defaults to the site's currency; a caller converting a charge
taken in another currency passes that currency so the divisor matches it.
Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing. Give maxEntries a value to bound the cache: storing a new key at the cap drops the oldest-stored entry first. Bound any cache whose keys can be chosen from outside (e.g. unknown session cookies), so a flood of unique keys cannot grow it without limit. Leave it unset for caches whose keyspace our own data already bounds — the keyed entity caches rely on holding every row of their table.
Remove duplicate values (by reference/value equality), keeping first
occurrences in order. Curried adapter over @std/collections.distinct.
Remove duplicates by a key function, keeping first occurrences in order.
Curried adapter over @std/collections.distinctBy.
Make a deterministic base slug unique by appending -2, -3, … until one
is free. Unlike generateUniqueSlug (random 5-char slugs), this keeps
a human-readable base — the news permalink yyyy-MM-dd-post-name — and only
disambiguates on collision (two same-day posts with the same name).
Convert a UTC ISO datetime string to a datetime-local input value (YYYY-MM-DDTHH:MM) in the given timezone. Used for pre-populating form inputs with timezone-adjusted values.
Parse a UTC ISO string into a ZonedDateTime in the given timezone
Validate a normalized slug. Returns error message or null.
Replace each markdown link whose target matches matcher with its plain
text. Used to strip links the viewer isn't allowed to open (e.g. owner-only
admin pages) before rendering — a rendered link is a promise that it works,
so a viewer who can't follow it gets the words without the link.
Why cached data was cleared. Only a committed write needs primary refills.
A dependsOn entry accepted by cachedTable / cachedEntityTable.
Error facts routed to the applicable sinks.
Log categories for debug logging
Slug-with-index pair. Index is the blind-index type computeIndex
produces (a BlindIndex for the real tables).
Remove one registration from the registry (handed back by the register functions; production callers register for the process's lifetime and drop it, tests must call it so their entries never outlive the test).
Milliseconds in one day — for whole-day arithmetic on epoch times.
Error code strings for use in logError calls
Human-readable labels for error codes (shown in admin activity log)
Join an array of strings into a single string (curried reduce shorthand). Replaces the common pattern: reduce((acc: string, s: string) => acc + s, "")
Register a cache stat provider (called at module load time)
Register an extra full-clear to run with a write cause when every cache is
reset. Only needed by caches without a table registration;
resetAllCaches already fires every table-registered invalidator.
Run a function within a pending-work scope. Whatever fn resolves to, the
queue is drained once more on the way out: an error logged after the
request's own flush (e.g. while the response is finalised) still queues
work, and work that outlived its request would complete during whatever
runs next — on Bunny that's a killed fetch, in tests a sanitizer failure
in an unrelated test.
Run a function with a request-scoped random ID for log correlation
Sum an array of numbers (identity selector shorthand for sumOf). Replaces the common pattern: reduce((acc, n) => acc + n, 0)
Keep non-critical error notifications behind a command's critical work. Nested callers join the outer queue so only the outermost boundary flushes.
Usage
import * as mod from "docs/utilities.ts";