Utilities
Small helpers that come with proc, for the jobs scripts keep needing.
range
import { range } from "@j50n/proc";
console.log(await range({ to: 3 }).collect()); // stops before `to`
console.log(await range({ from: 1, until: 3 }).collect()); // may end on `until`
console.log(await range({ from: 10, to: 0, step: -3 }).collect());
console.log(await range({ from: 1, to: Infinity }).take(3).collect());
[ 0, 1, 2 ]
[ 1, 2, 3 ]
[ 10, 7, 4, 1 ]
[ 1, 2, 3 ]
range counts from from (default 0) by step (default 1), as an
Enumerable. With to it stops before to; with until it includes until
when a step lands on it. A negative step counts down, a step that moves away
from the limit gives nothing, and a step of 0 throws RangeError. Numbers are
made as they are read, so to: Infinity is fine with take. Fractional steps
gather floating-point error: { until: 0.3, step: 0.1 } stops at 0.2.
sleep and the time constants
import { MINUTES, SECONDS, sleep } from "@j50n/proc";
console.log(`${5 * MINUTES} ms in five minutes`);
// Retry a flaky step, waiting longer each time.
let attempts = 0;
async function flaky() {
attempts += 1;
await sleep(1); // stands in for the real work
if (attempts < 3) throw new Error("not yet");
return "ok";
}
for (let wait = 0.01 * SECONDS;; wait *= 2) {
try {
console.log(`${await flaky()} after ${attempts} attempts`);
break;
} catch {
await sleep(wait);
}
}
300000 ms in five minutes
ok after 3 attempts
sleep(ms) resolves after ms milliseconds without blocking anything else.
SECONDS, MINUTES, HOURS, DAYS, and WEEKS are plain numbers of
milliseconds, for sleep, cache, or anything else that takes milliseconds:
sleep(2 * SECONDS).
shuffle
import { enumerate, shuffle } from "@j50n/proc";
// Run the tests in a random order. shuffle() changes the array in place.
const tests = ["parse", "render", "save", "load"];
shuffle(tests);
await enumerate(tests).forEach((test) => console.log(test)); // order varies
shuffle reorders an array in place and returns nothing. It uses Math.random,
so it is not for anything that must be unpredictable, such as tokens.
concat and concatLines
import { concat, concatLines, read } from "@j50n/proc";
// The whole file as one Uint8Array.
const bytes = concat(await read("fruit.txt").collect());
console.log(bytes.length);
// Lines of bytes back into text, with "\n" after each.
const encoder = new TextEncoder();
const text = concatLines([encoder.encode("one"), encoder.encode("two")]);
console.log(JSON.stringify(new TextDecoder().decode(text)));
20
"one\ntwo\n"
concat joins byte arrays into one; concatLines does the same with a "\n"
after each. Given a single array, concat returns that array itself, not a
copy. These are not Enumerable.concat, which appends one sequence to another.
cache
cache(key, compute, { timeout }) returns the value stored under key if it is
younger than timeout (default 24 hours); otherwise it calls compute, stores
the result, and returns it. The values live in Deno KV’s default database, so
they last across runs, which is the point: an expensive lookup is done once a
day, not every time the script runs.
This is a fragment, not a checked example, because Deno KV is unstable: the
program needs --unstable-kv (or "unstable": ["kv"] in deno.json), and the
book’s examples run without it. Without the flag, cache throws a TypeError
that says so.
import { cache, HOURS, run } from "@j50n/proc";
const branches = await cache(
["git", "remote-branches"],
() => run("git", "ls-remote", "--heads", "origin").lines.collect(),
{ timeout: 4 * HOURS },
);
null and undefined are never stored, so compute runs every time for them.
A value must fit in a KV entry (structured-cloneable, at most 64 KiB) to be
cached; one that doesn’t is returned without being stored, so compute runs
every time for it. Deno KV deletes each entry once it is older than the timeout
it was stored with. Two calls that miss at the same time both compute.
To recompute whatever is stored, pass refresh: true: compute runs, and its
result is stored as on a miss, so later calls read it. A script’s --refresh
flag maps straight onto it. A timeout of 0 also skips the read, but stores
nothing: use it for a call that shouldn’t touch the cache at all. A refresh
whose result can’t be stored removes the old entry, so a later call computes
again rather than getting the value the refresh replaced.
A value read back from the cache is a structured clone. A class instance comes back as a plain object, without its methods or getters, though its type still says it is the class; cache plain data.
Deno picks which database “default” means. With a deno.json, every script in
that project shares one, so two scripts that both use the key "config" get
each other’s values; without one, each main script has its own. The database is
a SQLite file under Deno’s cache directory (DENO_DIR), unencrypted, and an
entry stays in it until it expires, so don’t cache secrets such as tokens. See
the API reference.
debug
import { debug, read } from "@j50n/proc";
// Print each item as it passes this point, then carry on.
const b = await read("fruit.txt")
.lines
.transform(debug<string>)
.filter((fruit) => fruit.startsWith("b"))
.collect();
console.log(b);
"cherry"
"apple"
"banana"
[ "banana" ]
debug is a step that prints each item as JSON, in blue, as it passes, and
hands it on unchanged. It prints with console.log, so the lines land on stdout
among the program’s own output. Remove it when you’re done; an item JSON can’t
hold (a BigInt) throws.
isString
import { isString } from "@j50n/proc";
const values: unknown[] = ["text", 42, new String("boxed")];
console.log(values.map(isString));
[ true, false, false ]
isString is typeof value === "string" as a type guard, so TypeScript narrows
the value after it. A String object is not a string by this test.