Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.