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.
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.
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. Loops 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 currency is GBP)
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 an error context into a human-readable activity log message
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).
Treats the write as unconditional (INSERT semantics): always fires column-gated entries too.
Fire registered cache invalidators for table, respecting column gates.
Remove null and undefined values from array
ISO timestamp a fixed duration before the current time.
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.
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();
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).
Redact dynamic segments from paths for privacy-safe logging Replaces:
Curried reduce
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.
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.
Render markdown to HTML (block-level: paragraphs, lists, etc.). Raw HTML is escaped and unsafe URLs are stripped.
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.
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
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)
Create a TTL (Time-To-Live) cache. Entries expire after ttlMs milliseconds. Accepts an optional clock function for testing.
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.
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).
Verb of a mutating SQL statement
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, "")
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)
Usage
import * as mod from "docs/utilities.ts";