Skip to content

Basic Usage ​

Start at package root for common utilities. Move to a category entry point for the broader typed toolkit. Arsenal keeps helpers that improve readability, narrowing, immutable updates, or edge-case consistency; exact platform aliases stay out.

ts
import { groupBy, retry } from '@vielzeug/arsenal';

const users = [
  { id: 'a1', role: 'admin' },
  { id: 'u1', role: 'user' },
  { id: 'u2', role: 'user' },
];

const byRole = groupBy(users, (user) => user.role);
const health = await retry(() => fetch('/health').then((response) => response.json()));

console.log(byRole, health);

Use category imports for APIs absent from root:

ts
import { fuzzyFilter } from '@vielzeug/arsenal/array';
import { taskPool } from '@vielzeug/arsenal/async';
import { cache } from '@vielzeug/arsenal/cache';
import { tryParseJson } from '@vielzeug/arsenal/object';

Transform Collections ​

Use /array for transforms that preserve input immutability. filterMap combines mapping and omission; indexBy and groupBy build lookup structures without mutation.

ts
import { filterMap, indexBy, sort } from '@vielzeug/arsenal/array';

const products = [
  { id: 'p1', price: 20, published: true },
  { id: 'p2', price: 10, published: false },
  { id: 'p3', price: 15, published: true },
];

const publishedLabels = filterMap(products, (product) =>
  product.published ? `${product.id}: ${product.price}` : undefined,
);
const byId = indexBy(products, (product) => product.id);
const byPrice = sort(products, (product) => product.price);

console.log(publishedLabels, byId, byPrice);

Page an In-Memory List ​

Use paginate when the whole list is local and a view needs one page plus its totals.

ts
import { paginate } from '@vielzeug/arsenal/array';

const rows = Array.from({ length: 45 }, (_, index) => ({ id: index + 1 }));

const first = paginate(rows, { pageSize: 10 });
const past = paginate(rows, { page: 9, pageSize: 10 });

console.log(first.totalPages, past.items); // 5 []

page is echoed rather than clamped, so a stale page number reads as an empty page instead of silently jumping elsewhere. Pass { pageOffset: 0 } for a zero-based caller and the envelope comes back in that same base.

Sort by Grid Criteria ​

sort accepts the ordering a data grid already holds: an ordered list of column paths, each with a direction.

ts
import { sort } from '@vielzeug/arsenal/array';

const rows = [
  { address: { city: 'Berlin' }, age: 41 },
  { address: { city: 'Aachen' }, age: 30 },
  { address: { city: 'Berlin' }, age: 27 },
];

const ordered = sort(rows, [{ key: 'address.city' }, { key: 'age', direction: 'desc' }]);

Paths are resolved safely, so nested and indexed columns work and prototype access is refused. The first criterion that differs decides the order and ties keep their input order. Keep each column typed: a column mixing numbers and strings throws a TypeError rather than guessing an order.

Search Explicit Fields ​

Search string arrays directly. Object collections require select, so callers define exactly what can match.

ts
import { fuzzyFilter, fuzzyScore } from '@vielzeug/arsenal/array';

const users = [
  { email: 'alice@example.com', name: 'Alice' },
  { email: 'bob@example.com', name: 'Bob' },
];

const matches = fuzzyFilter(users, 'alice', { select: (user) => [user.name, user.email] });
const ranked = fuzzyScore(users, 'ali', { select: (user) => user.name });

Work with Object Data ​

Use /object for paths, key selection, stable cache keys, and object transforms.

ts
import { getPathOr, hash, omit, pick } from '@vielzeug/arsenal/object';

const config = { api: { host: 'localhost', port: 3000 }, debug: true };
const port = getPathOr(config, 'api.port', 8080);
const publicConfig = pick(config, ['api']);
const productionConfig = omit(config, ['debug']);
const key = hash({ port, productionConfig });

console.log(publicConfig, key);

Parse and Validate JSON ​

tryParseJson distinguishes syntax failure from schema failure. Treat successful values as unknown, then validate with Spell or application code.

ts
import { tryParseJson } from '@vielzeug/arsenal/object';
import { s } from '@vielzeug/spell';

const User = s.object({ id: s.string(), name: s.string() });
const parsed = tryParseJson(raw);

if (!parsed.ok) throw parsed.error;

const user = User.parse(parsed.value);

Bound Concurrent Work ​

Use parallel for one finite collection. Use taskPool when tasks arrive over time or need disposal.

ts
import { parallel, taskPool } from '@vielzeug/arsenal/async';

const metadata = await parallel(urls, (url) => fetch(url).then((response) => response.json()), { limit: 4 });

const pool = taskPool({ concurrency: 2 });
const profile = await pool.run((signal) => fetch('/profile', { signal }).then((response) => response.json()));

await pool.idle();
pool.dispose();

console.log(metadata, profile);

Cache Loaded Values ​

Use cache for process-local values. Keys retain native Map identity. getOrLoad deduplicates concurrent loads for one key.

ts
import { cache } from '@vielzeug/arsenal/cache';

type Profile = { id: string; name: string };

const profiles = cache<string, Profile>({ capacity: 100, ttlMs: 60_000 });
const profile = await profiles.getOrLoad('me', () => fetch('/profile').then((response) => response.json()));

console.log(profiles.entries());
profiles.delete('me');
const freshProfile = await profiles.getOrLoad('me', () => fetch('/profile').then((response) => response.json()));

console.log(profile, freshProfile);

Test Deterministic Randomness ​

Random helpers use cryptographic entropy by default. Pass RandomSource in tests when output must be deterministic.

ts
import { random, randomFloat, type RandomSource } from '@vielzeug/arsenal/random';

const source: RandomSource = { next: () => 0.5 };

randomFloat(source); // 0.5
random(1, 4, source); // 3

Encode Text for URLs and QR Codes ​

Share codes ride in URLs and QR payloads, where +, /, and padding = break consumers. The base64url codecs are environment-independent: no btoa, no Buffer, so the same encode/decode pair works in the browser, in workers, and in Node.

ts
import { base64UrlToText, textToBase64Url } from '@vielzeug/arsenal';

const code = textToBase64Url(JSON.stringify({ seat: 4, version: 2 }));
// eyJzZWF0Ijo0LCJ2ZXJzaW9uIjoyfQ: safe in a query string or QR

const payload = JSON.parse(base64UrlToText(code));

Decoding throws on characters outside the base64url alphabet, so corrupted input fails loudly instead of decoding to garbage.

Compress Share Payloads ​

Compress UTF-8 bytes before Base64url encoding. Set a decoded-byte limit before accepting compressed input from another device.

ts
import { base64UrlToBytes, bytesToBase64Url, compressBytes, decompressBytes } from '@vielzeug/arsenal';

const payload = JSON.stringify({ name: 'Hunters', notes: 'An expedition through Alborea. '.repeat(20) });
const compressed = await compressBytes(new TextEncoder().encode(payload));
const code = bytesToBase64Url(compressed);
const bytes = await decompressBytes(base64UrlToBytes(code), { maxOutputBytes: 8192 });
const restored = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
console.log(restored === payload);

Both helpers default to deflate-raw. You can explicitly select gzip or deflate; use the same format at both ends. The helpers reject when native streams do not support the selected format. They do not choose a fallback or define a wire protocol.

For a complete versioned envelope over these primitives — prefix, compression mode marker, byte budgets on both sides — see pack / unpack.

Compression can enlarge small inputs. Compare the final encoded strings, including any format marker, before choosing a representation. Bound input size before decoding Base64url and validate decompressed JSON against your schema. The decompression limit bounds retained output, not native stream working memory.

Working with Other Vielzeug Libraries ​

Use Spell after tryParseJson for typed external data. Use Vault instead of cache when data must survive reloads or process restart.

ts
import { tryParseJson } from '@vielzeug/arsenal/object';
import { s } from '@vielzeug/spell';

const Settings = s.object({ theme: s.string() });
const parsed = tryParseJson(rawSettings);
const settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };

Best Practices ​

  • Import common utilities from package root.
  • Import the broader toolkit from category entry points.
  • Prefer Arsenal when a helper improves typing, intent, immutable updates, or edge-case behavior.
  • Use the platform directly for exact aliases such as crypto.randomUUID() and Array.prototype.flat().
  • Pass select for every fuzzy search over objects.
  • Validate parsed JSON before using it as application data.
  • Use parallel for finite batches and taskPool for ongoing work.
  • Dispose task pools when their owner ends.
  • Use cache only for in-memory data.
  • Inject RandomSource in deterministic tests.