@vielzeug/codex 2.1.4 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors.js +0 -3
- package/dist/errors.js.map +1 -1
- package/dist/snapshot.js.map +1 -1
- package/dist/tools/packages.js +2 -3
- package/dist/tools/packages.js.map +1 -1
- package/dist/tools/refine.js +2 -2
- package/dist/tools/refine.js.map +1 -1
- package/dist/tools/schema.js +2 -0
- package/dist/tools/schema.js.map +1 -1
- package/package.json +6 -1
- package/data/catalog.json +0 -1689
- package/data/llms-full.txt +0 -25771
- package/data/llms.txt +0 -40
- package/data/manifest.json +0 -8
- package/data/packages/arsenal.json +0 -210
- package/data/packages/assay.json +0 -40
- package/data/packages/clockwork.json +0 -67
- package/data/packages/codex.json +0 -43
- package/data/packages/coins.json +0 -103
- package/data/packages/conduit.json +0 -60
- package/data/packages/courier.json +0 -58
- package/data/packages/dnd.json +0 -77
- package/data/packages/familiar.json +0 -40
- package/data/packages/flux.json +0 -93
- package/data/packages/forge.json +0 -84
- package/data/packages/herald.json +0 -108
- package/data/packages/keymap.json +0 -59
- package/data/packages/ledger.json +0 -57
- package/data/packages/lingua.json +0 -68
- package/data/packages/necromancer.json +0 -50
- package/data/packages/orbit.json +0 -107
- package/data/packages/ore.json +0 -73
- package/data/packages/prism.json +0 -67
- package/data/packages/pulse.json +0 -60
- package/data/packages/refine.json +0 -12
- package/data/packages/ripple.json +0 -83
- package/data/packages/rune.json +0 -80
- package/data/packages/sandbox.json +0 -40
- package/data/packages/scout.json +0 -60
- package/data/packages/scroll.json +0 -114
- package/data/packages/sourcerer.json +0 -74
- package/data/packages/spell.json +0 -134
- package/data/packages/tempo.json +0 -81
- package/data/packages/vault.json +0 -87
- package/data/packages/ward.json +0 -113
- package/data/packages/wayfinder.json +0 -113
- package/data/refine.json +0 -11926
- package/data/search.json +0 -1436
package/data/llms.txt
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Vielzeug
|
|
2
|
-
|
|
3
|
-
> 32 focused TypeScript packages. Version: 2.1.3
|
|
4
|
-
|
|
5
|
-
Install any package independently: `pnpm add @vielzeug/<name>`
|
|
6
|
-
|
|
7
|
-
## Packages
|
|
8
|
-
|
|
9
|
-
- [@vielzeug/arsenal](/arsenal/): Tree-shakeable TypeScript utilities with focused category entry points for arrays, async work, caching, objects, strings, math, and guards.
|
|
10
|
-
- [@vielzeug/assay](/assay/): Scoped DOM queries, exact event dispatch, and cancellable async waiting for browser tests.
|
|
11
|
-
- [@vielzeug/clockwork](/clockwork/): Framework-neutral typed state machines with pure transitions, actor-owned runtime work, timers, invokes, and explicit effects.
|
|
12
|
-
- [@vielzeug/codex](/codex/): Local MCP access to Vielzeug documentation and package metadata.
|
|
13
|
-
- [@vielzeug/coins](/coins/): Exact bigint monetary arithmetic with explicit currency definitions, decimal strings, allocation, exchange, formatting, and JSON boundaries.
|
|
14
|
-
- [@vielzeug/conduit](/conduit/): Dependency-first asynchronous dependency injection with typed tokens, lifecycle scopes, startup validation, and deterministic disposal.
|
|
15
|
-
- [@vielzeug/courier](/courier/): A framework-neutral fetch client with explicit cache keys, direct mutations, and abortable streams.
|
|
16
|
-
- [@vielzeug/dnd](/dnd/): Framework-agnostic drag-and-drop. Drop zones with MIME filtering, sortable lists with drag handles, and explicit connected scopes — zero dependencies.
|
|
17
|
-
- [@vielzeug/familiar](/familiar/): Typed ES module Worker pools with cancellation, priority scheduling, streaming, and test utilities.
|
|
18
|
-
- [@vielzeug/flux](/flux/): Reusable push streams with subscription-owned cancellation, bounded buffering, and optional ecosystem adapters.
|
|
19
|
-
- [@vielzeug/forge](/forge/): Framework-agnostic immutable form state with focused object fields and explicit validation results.
|
|
20
|
-
- [@vielzeug/herald](/herald/): Typed temporal event delivery with sync subscriptions, async waiting, streams, pipes, and AbortSignal lifecycle.
|
|
21
|
-
- [@vielzeug/keymap](/keymap/): Target-local keyboard shortcut manager with chords, event-aware guards, modifier aliases, and terminal disposal.
|
|
22
|
-
- [@vielzeug/ledger](/ledger/): Serialized reversible command history with cancellation ownership and atomic reactive snapshots.
|
|
23
|
-
- [@vielzeug/lingua](/lingua/): Framework-neutral locale catalogs, typed translations, and explicit plural messages.
|
|
24
|
-
- [@vielzeug/necromancer](/necromancer/): Lifecycle-owned Web Animations API primitives for native playback, groups, and additive FLIP transitions.
|
|
25
|
-
- [@vielzeug/orbit](/orbit/): Dependency-free floating positioning with lifecycle-owned geometry and middleware.
|
|
26
|
-
- [@vielzeug/ore](/ore/): Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, observers, and testing utilities.
|
|
27
|
-
- [@vielzeug/prism](/prism/): Reactive SVG charting library — line, bar, and area charts. Signal-driven updates, CSS-themeable, accessible.
|
|
28
|
-
- [@vielzeug/pulse](/pulse/): Explicitly connected, typed WebSocket sessions with scoped channels, presence, reconnect restoration, and heartbeat.
|
|
29
|
-
- [@vielzeug/refine](/refine/): Accessible, themeable web components built with Ore for framework and vanilla DOM apps.
|
|
30
|
-
- [@vielzeug/ripple](/ripple/): Framework-agnostic signals, derived values, effects, scopes, watchers, and async resources.
|
|
31
|
-
- [@vielzeug/rune](/rune/): Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.
|
|
32
|
-
- [@vielzeug/sandbox](/sandbox/): Isolated iframe runtime with a typed postMessage bridge for safe execution of untrusted HTML — component previews, playgrounds, plugin sandboxes, and more.
|
|
33
|
-
- [@vielzeug/scout](/scout/): Trigram-indexed fuzzy search with per-field weights, match highlighting, and an optional reactive layer.
|
|
34
|
-
- [@vielzeug/scroll](/scroll/): Lightweight, framework-agnostic virtual list engine with variable heights, sticky headers, grid support, and reactive integration.
|
|
35
|
-
- [@vielzeug/sourcerer](/sourcerer/): Framework-agnostic collection sources for local, page, cursor, and infinite pagination.
|
|
36
|
-
- [@vielzeug/spell](/spell/): Schema validation with explicit sync/async checks, portable definitions, JSON Schema export, and tree-shakeable entry points.
|
|
37
|
-
- [@vielzeug/tempo](/tempo/): Explicit Temporal parsing, timezone-safe arithmetic, and localized date/time formatting for TypeScript.
|
|
38
|
-
- [@vielzeug/vault](/vault/): Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.
|
|
39
|
-
- [@vielzeug/ward](/ward/): Typed authorization policies with wildcard matching, deterministic precedence, and decision tracing.
|
|
40
|
-
- [@vielzeug/wayfinder](/wayfinder/): Framework-agnostic client-side router with typed params, async data loading, middleware, leave guards, and View Transitions support.
|
package/data/manifest.json
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export * from './errors';\nexport * from './array/chunk';\nexport * from './array/filterMap';\nexport * from './array/groupBy';\nexport * from './array/indexBy';\nexport * from './array/partition';\nexport * from './array/sort';\nexport * from './array/uniq';\nexport * from './async/attempt';\nexport * from './async/parallel';\nexport * from './async/retry';\nexport * from './async/sleep';\nexport * from './function/debounce';\nexport * from './function/once';\nexport * from './function/pipe';\nexport * from './function/throttle';\nexport * from './guards/combinators';\nexport * from './guards/isDefined';\nexport * from './guards/isEqual';\nexport * from './guards/isNil';\nexport * from './guards/isPlainObject';\nexport * from './math/clamp';\nexport * from './math/range';\nexport * from './object/getPath';\nexport * from './object/hash';\nexport * from './object/omit';\nexport * from './object/pick';\nexport * from './random/uuid';\nexport * from './string/camelCase';\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Arsenal — Utility library for TypeScript\ndescription: Tree-shakeable TypeScript utilities with focused category entry points for arrays, async work, caching, objects, strings, math, and guards.\npackage: arsenal\ncategory: utilities\nkeywords: [utility, array, string, object, math, async, debounce, throttle, cache]\nexports: [chunk, groupBy, retry, debounce, clamp, isEqual, taskPool, cache, fuzzyFilter, tryParseJson]\nrelated: [tempo, sourcerer, spell, coins]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"arsenal\" />\n\n## Why Arsenal?\n\nArsenal keeps common utilities at package root and places specialized behavior behind category entry points. This keeps autocomplete focused while preserving one dependency and tree-shakeable modules.\n\n```ts\n// Before\nconst users = JSON.parse(raw).filter((user) => user.name.includes(query));\n\n// After\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n\nconst parsed = tryParseJson(raw);\nconst users = parsed.ok ? fuzzyFilter(parsed.value as User[], query, { select: (user) => user.name }) : [];\n```\n\n| Feature | Arsenal | lodash-es | Remeda |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"arsenal\" type=\"size\" /> | ~72 kB | ~18 kB |\n| Typed root utilities | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Category entry points | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Async task pool and cache | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Arsenal when** you need one typed utility dependency with focused subpaths for specialized behavior.\n\n**Consider narrower alternatives when** you need only platform APIs or a small functional subset.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/arsenal\n```\n\n```sh [npm]\nnpm install @vielzeug/arsenal\n```\n\n```sh [yarn]\nyarn add @vielzeug/arsenal\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { chunk, groupBy, retry } from '@vielzeug/arsenal';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { taskPool } from '@vielzeug/arsenal/async';\n\nconst pages = chunk([1, 2, 3, 4, 5], 2);\nconst byRole = groupBy([{ role: 'admin' }, { role: 'user' }], (user) => user.role);\n\nconst pool = taskPool({ concurrency: 2 });\nconst health = await pool.run((signal) => retry(() => fetch('/health', { signal }).then((response) => response.json())));\n\nconst responses = cache<string, unknown>({ ttlMs: 60_000 });\nconst profile = await responses.getOrLoad('/profile', () => fetch('/profile').then((response) => response.json()));\n\npool.dispose();\nconsole.log(pages, byRole, health, profile);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`chunk`**: common array/string chunking from package root\n- **`retry`**: retry async work with cancellation support from package root\n- **`taskPool`**: bounded, disposable concurrent work from `/async`\n- **`cache`**: identity-keyed TTL cache with async load deduplication from `/cache`\n- **`fuzzyFilter`**: explicit-field fuzzy filtering from `/array`\n- **`tryParseJson`**: preserve JSON syntax failures from `/object`\n- **`clamp`**: numeric bounds from package root\n- **`isEqual`**: structural equality from package root\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — validate `unknown` JSON data after `tryParseJson`.\n- [Vault](/vault/) — persistent storage; Arsenal cache is in-memory only.\n- [Tempo](/tempo/) — date/time utilities kept outside Arsenal.\n- [Coins](/coins/) — money formatting and currency conversion.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Arsenal — API Reference\ndescription: Reference for Arsenal root utilities and category entry points.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `chunk` | Split arrays or strings | Sync | Root export |\n| `groupBy` | Group values by key | Sync | Root export |\n| `retry` | Retry async work | Async | Rethrows final error |\n| `taskPool` | Bound concurrent tasks | Async | Available from `/async` |\n| `cache` | In-memory identity-keyed cache | Async | Available from `/cache` |\n| `fuzzyFilter` | Filter string or selected object fields | Sync | Object collections require `select` |\n| `fuzzyScore` | Rank string or selected object fields | Sync | Object collections require `select` |\n| `tryParseJson` | Preserve JSON syntax result | Sync | Returns `unknown` on success |\n| `getPath` | Optional object lookup | Sync | Available from `/object` |\n| `clamp` | Bound number to range | Sync | Root export |\n| `isEqual` | Structural equality | Sync | Root export |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/arsenal` | Curated common utilities |\n| `@vielzeug/arsenal/array` | Array transforms, sorting, fuzzy search |\n| `@vielzeug/arsenal/async` | Retry, cancellation, task pool, timing |\n| `@vielzeug/arsenal/cache` | In-memory cache and memoization |\n| `@vielzeug/arsenal/function` | Composition, timing, assertions |\n| `@vielzeug/arsenal/guards` | Predicate and type guard helpers |\n| `@vielzeug/arsenal/math` | Numeric and statistical helpers |\n| `@vielzeug/arsenal/object` | Paths, transforms, hash, JSON parse result |\n| `@vielzeug/arsenal/random` | Random selection and UUID helpers |\n| `@vielzeug/arsenal/string` | Text transforms and similarity |\n\n## Array\n\n### fuzzyFilter / fuzzyScore\n\n```ts\nfuzzyFilter(strings: readonly string[], query: string, options?: FuzzyOptions): string[]\nfuzzyFilter<T>(items: readonly T[], query: string, options: FuzzySelection<T>): T[]\nfuzzyScore(strings: readonly string[], query: string, options?: FuzzyOptions): ScoredResult<string>[]\nfuzzyScore<T>(items: readonly T[], query: string, options: FuzzySelection<T>): ScoredResult<T>[]\n```\n\n`fuzzyFilter` preserves input order. `fuzzyScore` orders results by descending score.\n\n```ts\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\n\nconst users = [{ email: 'alice@example.com', name: 'Alice' }];\nconst matches = fuzzyFilter(users, 'alice', { select: (user) => [user.name, user.email] });\n```\n\n---\n\n## Async\n\n### taskPool\n\n```ts\ninterface TaskPool {\n run<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>;\n idle(): Promise<void>;\n dispose(reason?: unknown): void;\n readonly active: number;\n readonly pending: number;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n}\n\ntaskPool(options?: { concurrency?: number }): TaskPool\n```\n\n`dispose()` aborts running cooperative tasks and rejects pending tasks.\n\n```ts\nimport { taskPool } from '@vielzeug/arsenal/async';\n\nconst pool = taskPool({ concurrency: 2 });\nconst user = await pool.run((signal) => fetch('/user', { signal }).then((response) => response.json()));\npool.dispose();\n```\n\n---\n\n## Cache\n\n### cache\n\n```ts\ninterface Cache<K, T> {\n get(key: K): T | undefined;\n set(key: K, value: T, options?: { ttlMs?: number }): void;\n getOrLoad(key: K, load: () => Promise<T>): Promise<T>;\n delete(key: K): boolean;\n clear(): void;\n readonly size: number;\n}\n\ncache<K, T>(options?: CacheOptions): Cache<K, T>\n```\n\nKeys use native `Map` identity. Expiry is lazy, evaluated by `get`, `getOrLoad`, and `size`.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\nconst profiles = cache<string, Profile>({ ttlMs: 60_000 });\nconst profile = await profiles.getOrLoad('me', loadProfile);\n```\n\n---\n\n## Object\n\n### tryParseJson\n\n```ts\ntype JsonParseResult = { ok: true; value: unknown } | { error: SyntaxError; ok: false };\n\ntryParseJson(text: string): JsonParseResult\n```\n\nUse a schema validator after success to refine `unknown` data.\n\n```ts\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n\nconst result = tryParseJson(raw);\nif (!result.ok) throw result.error;\n```\n\n### getPath\n\n```ts\ngetPath<T extends Record<string, unknown>, P extends string>(item: T, path: P): PathValue<T, P> | undefined\ngetPathOr<T extends Record<string, unknown>, P extends string, F>(item: T, path: P, fallback: F): PathValue<T, P> | F\nrequirePath<T extends Record<string, unknown>, P extends string>(item: T, path: P): Exclude<PathValue<T, P>, undefined>\n```\n\n## Types\n\n```ts\ntype FuzzyOptions = {\n normalize?: boolean;\n threshold?: number;\n};\n\ntype FuzzySelection<T> = FuzzyOptions & {\n select: (item: T) => string | readonly string[];\n};\n\ntype ScoredResult<T> = { item: T; score: number };\n\ntype CacheOptions = {\n capacity?: number;\n now?: () => number;\n ttlMs?: number;\n};\n```\n\n## Errors\n\n- `RangeError` — invalid numeric bounds, capacity, concurrency, or retry count.\n- `TypeError` — invalid value types, unsupported comparison, or required path missing.\n- `ArsenalSerializationError` — memo or hash cannot serialize supplied input.\n",
|
|
6
|
-
"usage": "---\ntitle: Arsenal — Usage Guide\ndescription: Use Arsenal root utilities for common work and category entry points for specialized collection, async, cache, object, and string behavior.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart at package root for common transforms. Move to a category entry point when code needs specialized behavior. This keeps imports readable and bundles focused.\n\n```ts\nimport { chunk, groupBy, retry } from '@vielzeug/arsenal';\n\nconst users = [\n { id: 'a1', role: 'admin' },\n { id: 'u1', role: 'user' },\n { id: 'u2', role: 'user' },\n];\n\nconst pages = chunk(users, 2);\nconst byRole = groupBy(users, (user) => user.role);\nconst health = await retry(() => fetch('/health').then((response) => response.json()));\n\nconsole.log(pages, byRole, health);\n```\n\nUse category imports for APIs absent from root:\n\n```ts\nimport { fuzzyFilter } from '@vielzeug/arsenal/array';\nimport { taskPool } from '@vielzeug/arsenal/async';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { tryParseJson } from '@vielzeug/arsenal/object';\n```\n\n## Transform Collections\n\nUse `/array` for transforms that preserve input immutability. `filterMap` combines mapping and omission; `indexBy` and `groupBy` build lookup structures without mutation.\n\n```ts\nimport { filterMap, indexBy, sort } from '@vielzeug/arsenal/array';\n\nconst products = [\n { id: 'p1', price: 20, published: true },\n { id: 'p2', price: 10, published: false },\n { id: 'p3', price: 15, published: true },\n];\n\nconst publishedLabels = filterMap(products, (product) => (product.published ? `${product.id}: ${product.price}` : undefined));\nconst byId = indexBy(products, (product) => product.id);\nconst byPrice = sort(products, (product) => product.price);\n\nconsole.log(publishedLabels, byId, byPrice);\n```\n\n## Search Explicit Fields\n\nSearch string arrays directly. Object collections require `select`, so callers define exactly what can match.\n\n```ts\nimport { fuzzyFilter, fuzzyScore } from '@vielzeug/arsenal/array';\n\nconst users = [\n { email: 'alice@example.com', name: 'Alice' },\n { email: 'bob@example.com', name: 'Bob' },\n];\n\nconst matches = fuzzyFilter(users, 'alice', { select: (user) => [user.name, user.email] });\nconst ranked = fuzzyScore(users, 'ali', { select: (user) => user.name });\n```\n\n## Work with Object Data\n\nUse `/object` for paths, key selection, stable cache keys, and object transforms.\n\n```ts\nimport { getPathOr, hash, omit, pick } from '@vielzeug/arsenal/object';\n\nconst config = { api: { host: 'localhost', port: 3000 }, debug: true };\nconst port = getPathOr(config, 'api.port', 8080);\nconst publicConfig = pick(config, ['api']);\nconst productionConfig = omit(config, ['debug']);\nconst key = hash({ port, productionConfig });\n\nconsole.log(publicConfig, key);\n```\n\n## Parse and Validate JSON\n\n`tryParseJson` distinguishes syntax failure from schema failure. Treat successful values as `unknown`, then validate with Spell or application code.\n\n```ts\nimport { tryParseJson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({ id: s.string(), name: s.string() });\nconst parsed = tryParseJson(raw);\n\nif (!parsed.ok) throw parsed.error;\n\nconst user = User.parse(parsed.value);\n```\n\n## Bound Concurrent Work\n\nUse `parallel` for one finite collection. Use `taskPool` when tasks arrive over time or need disposal.\n\n```ts\nimport { parallel, taskPool } from '@vielzeug/arsenal/async';\n\nconst metadata = await parallel(urls, (url) => fetch(url).then((response) => response.json()), { limit: 4 });\n\nconst pool = taskPool({ concurrency: 2 });\nconst profile = await pool.run((signal) => fetch('/profile', { signal }).then((response) => response.json()));\n\nawait pool.idle();\npool.dispose();\n\nconsole.log(metadata, profile);\n```\n\n## Cache Loaded Values\n\nUse `cache` for process-local values. Keys retain native `Map` identity. `getOrLoad` deduplicates concurrent loads for one key.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\ntype Profile = { id: string; name: string };\n\nconst profiles = cache<string, Profile>({ capacity: 100, ttlMs: 60_000 });\nconst profile = await profiles.getOrLoad('me', () => fetch('/profile').then((response) => response.json()));\n\nprofiles.delete('me');\nconst freshProfile = await profiles.getOrLoad('me', () => fetch('/profile').then((response) => response.json()));\n\nconsole.log(profile, freshProfile);\n```\n\n## Test Deterministic Randomness\n\nRandom helpers use cryptographic entropy by default. Pass `RandomSource` in tests when output must be deterministic.\n\n```ts\nimport { random, type RandomSource } from '@vielzeug/arsenal/random';\n\nconst source: RandomSource = { next: () => 0.5 };\n\nrandom(1, 4, source); // 3\n```\n\n## Working with Other Vielzeug Libraries\n\nUse Spell after `tryParseJson` for typed external data. Use Vault instead of `cache` when data must survive reloads or process restart.\n\n```ts\nimport { tryParseJson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({ theme: s.string() });\nconst parsed = tryParseJson(rawSettings);\nconst settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };\n```\n\n## Best Practices\n\n- Import common transforms from package root.\n- Import specialized APIs from category entry points.\n- Pass `select` for every fuzzy search over objects.\n- Validate parsed JSON before using it as application data.\n- Use `parallel` for finite batches and `taskPool` for ongoing work.\n- Dispose task pools when their owner ends.\n- Use `cache` only for in-memory data.\n- Inject `RandomSource` in deterministic tests.\n",
|
|
7
|
-
"examples": "---\ntitle: Arsenal — Examples\ndescription: Practical examples and recipes for arsenal.\n---\n\n## Examples\n\n- [Array utilities](./examples/array.md)\n- [Async utilities](./examples/async.md)\n- [Cache utilities](./examples/cache.md)\n- [Function utilities](./examples/function.md)\n- [Guards / Typed predicates](./examples/typed.md)\n- [Math utilities](./examples/math.md)\n- [Object utilities](./examples/object.md) — includes `getPath`, `tryParseJson`, `stringify`, `diff`, `deepMerge`\n- [Random utilities](./examples/random.md)\n- [String utilities](./examples/string.md)\n"
|
|
8
|
-
},
|
|
9
|
-
"examples": [
|
|
10
|
-
{
|
|
11
|
-
"id": "array-chunk",
|
|
12
|
-
"code": "import { chunk } from '@vielzeug/arsenal'\n\nconst numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n\nconsole.log('Original:', numbers)\nconsole.log('Chunks of 3:', chunk(numbers, 3))\nconsole.log('Chunks of 4:', chunk(numbers, 4))\n\n// Practical use case: Batch processing\nconst userIds = [101, 102, 103, 104, 105, 106, 107, 108]\nconst batches = chunk(userIds, 3)\nconsole.log('User ID batches:', batches)",
|
|
13
|
-
"name": "chunk - Split array into chunks"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": "array-filter",
|
|
17
|
-
"code": "import { filterMap } from '@vielzeug/arsenal'\n\nconst users = [\n { name: 'Alice', age: 25, active: true },\n { name: 'Bob', age: 30, active: false },\n { name: 'Charlie', age: 35, active: true },\n { name: 'David', age: 28, active: true }\n]\n\nconst activeUsers = filterMap(users, user =>\n user.active ? user : undefined\n)\nconsole.log('Active users:', activeUsers)\n\nconst over30 = filterMap(users, user =>\n user.age > 30 ? user : undefined\n)\nconsole.log('Users over 30:', over30)\n\nconst activeNames = filterMap(users, user =>\n user.active ? user.name : undefined\n)\nconsole.log('Active user names:', activeNames)",
|
|
18
|
-
"name": "filterMap - Filter and map array elements"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "array-group",
|
|
22
|
-
"code": "import { groupBy } from '@vielzeug/arsenal'\n\nconst items = [\n { type: 'fruit', name: 'apple', price: 1.2 },\n { type: 'vegetable', name: 'carrot', price: 0.8 },\n { type: 'fruit', name: 'banana', price: 0.5 },\n { type: 'vegetable', name: 'broccoli', price: 1.5 },\n { type: 'fruit', name: 'orange', price: 0.9 }\n]\n\nconst byType = groupBy(items, item => item.type)\nconsole.log('Grouped by type:', byType)\n\nconst byPriceRange = groupBy(items, item =>\n item.price < 1 ? 'cheap' : 'expensive'\n)\nconsole.log('Grouped by price:', byPriceRange)",
|
|
23
|
-
"name": "groupBy - Group array by key"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"id": "array-map",
|
|
27
|
-
"code": "import { filterMap } from '@vielzeug/arsenal'\n\nconst numbers = [1, 2, 3, 4, 5]\n\nconst doubled = filterMap(numbers, number => number * 2)\nconsole.log('Doubled:', doubled)\n\nconst strings = filterMap(numbers, number => `Number: ${number}`)\nconsole.log('Formatted:', strings)\n\nconst evenDoubled = filterMap(numbers, number =>\n number % 2 === 0 ? number * 2 : undefined\n)\nconsole.log('Even numbers doubled:', evenDoubled)",
|
|
28
|
-
"name": "filterMap - Transform array elements"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "array-search",
|
|
32
|
-
"code": "import { fuzzyFilter, fuzzyScore } from '@vielzeug/arsenal/array'\n\nconst users = [\n { name: 'Alice Johnson', role: 'admin' },\n { name: 'Bob Smith', role: 'user' },\n { name: 'Charlie Brown', role: 'user' },\n]\n\nconst byName = fuzzyFilter(users, 'alice', { select: user => user.name })\nconsole.log('Filtered:', byName)\n\nconst ranked = fuzzyScore(users, 'smith', { select: user => [user.name, user.role] })\nconsole.log('Ranked:', ranked)",
|
|
33
|
-
"name": "fuzzyFilter - Explicit searchable fields"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"id": "array-search-normalize",
|
|
37
|
-
"code": "import { fuzzyFilter, fuzzyScore } from '@vielzeug/arsenal/array'\n\nconst names = ['José', 'Élise', 'café', 'naïve', 'resume']\n\nconst noNorm = fuzzyFilter(names, 'jose', { threshold: 0.9 })\nconsole.log('normalize:false:', noNorm)\n\nconst withNorm = fuzzyFilter(names, 'jose', { normalize: true, threshold: 0.9 })\nconsole.log('normalize:true:', withNorm)\n\nconst scored = fuzzyScore(names, 'elise', { normalize: true, threshold: 0.5 })\nconsole.log('Scored:', scored)",
|
|
38
|
-
"name": "fuzzyFilter - Unicode normalization"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "array-uniq",
|
|
42
|
-
"code": "import { uniq } from '@vielzeug/arsenal'\n\nconst numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5]\nconsole.log('Unique numbers:', uniq(numbers))\n\nconst tags = ['javascript', 'react', 'vue', 'react', 'angular', 'vue']\nconsole.log('Unique tags:', uniq(tags))\n\n// Works with objects too (by reference)\nconst obj1 = { id: 1 }\nconst obj2 = { id: 2 }\nconst objects = [obj1, obj2, obj1, obj2]\nconsole.log('Unique objects:', uniq(objects))",
|
|
43
|
-
"name": "uniq - Remove duplicates"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"id": "async-attempt",
|
|
47
|
-
"code": "import { attempt, isFail, isOk, retry } from '@vielzeug/arsenal/async'\n\n// attempt() wraps any async function — never throws, always returns { ok, value|error }\nconst ok = await attempt(async () => {\n await new Promise(r => setTimeout(r, 10))\n return { id: 1, title: 'Buy groceries' }\n})\n\nif (isOk(ok)) {\n console.log('Success:', ok.value)\n}\n\n// Failure path — errors are captured, not thrown\nconst fail = await attempt(async () => {\n throw new Error('network timeout')\n})\n\nif (isFail(fail)) {\n console.log('Caught:', fail.error.message) // 'network timeout'\n}\n\n// Combine with retry() for resilient operations\nlet calls = 0\nconst result = await attempt(() =>\n retry(async () => {\n calls++\n if (calls < 3) throw new Error('not ready')\n return 'ready'\n }, { times: 5, delay: 10 })\n)\n\nconsole.log('ok?', result.ok) // true\nconsole.log('value:', result.value) // 'ready'\nconsole.log('calls:', calls) // 3",
|
|
48
|
-
"name": "attempt - Safe async execution with isFail/isOk helpers"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"id": "async-parallel",
|
|
52
|
-
"code": "import { parallel } from '@vielzeug/arsenal/async'\n\nconst items = [1, 2, 3, 4, 5, 6, 7, 8]\n\nconst results = await parallel(\n items,\n async item => {\n console.log(`Processing: ${item}`)\n await new Promise(resolve => setTimeout(resolve, 100))\n return item * 2\n },\n { limit: 2 }\n)\n\nconsole.log('Results:', results)",
|
|
53
|
-
"name": "parallel - Controlled parallel execution"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"id": "async-pool",
|
|
57
|
-
"code": "import { taskPool } from '@vielzeug/arsenal/async'\n\nconst pool = taskPool({ concurrency: 3 })\nconst tasks = Array.from({ length: 6 }, (_, index) =>\n pool.run(async () => {\n console.log(`Task ${index + 1} started`)\n await new Promise(resolve => setTimeout(resolve, 100))\n return `Result ${index + 1}`\n }),\n)\n\nconsole.log('All results:', await Promise.all(tasks))\nawait pool.idle()\npool.dispose()",
|
|
58
|
-
"name": "taskPool - Parallel execution with concurrency limit"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"id": "async-queue",
|
|
62
|
-
"code": "import { taskPool } from '@vielzeug/arsenal/async'\n\nconst pool = taskPool({ concurrency: 2 })\nconst tasks = [100, 50, 75, 30].map((delay, index) =>\n pool.run(async (signal) => {\n await new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, delay)\n signal.addEventListener('abort', () => {\n clearTimeout(timer)\n reject(signal.reason)\n }, { once: true })\n })\n return 'task-' + (index + 1)\n }),\n)\n\nconsole.log('After enqueue:', { active: pool.active, pending: pool.pending })\nconsole.log('Results:', await Promise.all(tasks))\nawait pool.idle()\npool.dispose()",
|
|
63
|
-
"name": "taskPool - Bounded concurrent work"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"id": "async-retry",
|
|
67
|
-
"code": "import { retry } from '@vielzeug/arsenal/async'\n\nlet attempts = 0\nconst unreliableOperation = async () => {\n attempts++\n console.log(`Attempt #${attempts}`)\n\n if (attempts < 3) {\n throw new Error('Failed!')\n }\n\n return 'Success!'\n}\n\ntry {\n const result = await retry(unreliableOperation, {\n times: 5,\n delay: 100\n })\n console.log('Result:', result)\n console.log('Total attempts:', attempts)\n} catch (err) {\n console.error('All retries failed:', err.message)\n}",
|
|
68
|
-
"name": "retry - Retry failed operations"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"id": "async-waitFor",
|
|
72
|
-
"code": "import { waitFor } from '@vielzeug/arsenal/async'\n\n// Simulate a value that becomes ready after a short delay\nlet ready = false\nsetTimeout(() => { ready = true }, 200)\n\nconsole.log('Waiting for ready...')\nawait waitFor(() => ready, { interval: 50, timeout: 2000 })\nconsole.log('Ready!')\n\n// Abort early with an external signal\nconst ac = new AbortController()\nsetTimeout(() => ac.abort(new Error('user cancelled')), 100)\n\ntry {\n await waitFor(() => false, {\n interval: 50,\n signal: ac.signal,\n timeout: 5000,\n })\n} catch (err) {\n console.log('Aborted:', err.message) // 'user cancelled'\n}",
|
|
73
|
-
"name": "waitFor - Poll until condition is true or timeout/abort fires"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"id": "function-debounce",
|
|
77
|
-
"code": "import { debounce } from '@vielzeug/arsenal/function'\n\n// --- Trailing (default) ---\nlet trailingCount = 0\nconst onSearch = debounce((q) => {\n trailingCount++\n console.log(`Trailing #${trailingCount}: \"${q}\"`)\n}, 200)\n\nonSearch('c')\nonSearch('ca')\nonSearch('cat') // only this fires after 200ms\n\n// --- Leading-only ---\nlet leadingCount = 0\nconst onSubmit = debounce((q) => {\n leadingCount++\n console.log(`Leading #${leadingCount}: \"${q}\"`)\n}, 200, { leading: true, trailing: false })\n\nonSubmit('first') // fires immediately\nonSubmit('second') // silenced (within 200ms window)\nonSubmit('third') // silenced\n\n// --- Leading + Trailing: fires on both edges ---\nlet bothCount = 0\nconst onBoth = debounce(() => {\n bothCount++\n console.log(`Both edge #${bothCount}`)\n}, 200, { leading: true, trailing: true })\n\nonBoth() // fires immediately (leading)\n// trailing edge fires after 200ms (bothCount becomes 2)\n\nsetTimeout(() => {\n console.log('Trailing fires:', trailingCount, '| Leading fires:', leadingCount, '| Both fires:', bothCount)\n // trailing: 1 | leading: 1 | both: 2\n}, 400)",
|
|
78
|
-
"name": "debounce - Trailing (default) and leading-edge options"
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"id": "function-memo",
|
|
82
|
-
"code": "import { memo } from '@vielzeug/arsenal/cache'\n\n// LRU cache capped at 3 entries — oldest evicted when full\nlet callCount = 0\nconst compute = memo(\n (n) => { callCount++; return n * n },\n { maxSize: 3 }\n)\n\nconsole.log(compute(2)) // 4 — computed\nconsole.log(compute(3)) // 9 — computed\nconsole.log(compute(4)) // 16 — computed\nconsole.log(compute(2)) // 4 — cache hit\nconsole.log('calls so far:', callCount) // 3\nconsole.log('cached entries:', compute.size) // 3\n\n// Adding a 4th entry evicts the oldest (key 2)\nconsole.log(compute(5)) // 25 — computed, evicts 2\nconsole.log('after 4th entry, size:', compute.size) // 3\n\n// Invalidate a specific entry\ncompute.invalidate(3)\nconsole.log('after invalidate(3), size:', compute.size) // 2\nconsole.log(compute(3)) // 9 — recomputed\nconsole.log('total calls:', callCount) // 5",
|
|
83
|
-
"name": "memo - LRU cache with size tracking and invalidation"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"id": "function-pipe",
|
|
87
|
-
"code": "import { pipe } from '@vielzeug/arsenal'\n\n// pipe: left-to-right function composition\nconst add5 = (n) => n + 5\nconst multiply2 = (n) => n * 2\nconst square = (n) => n * n\n\nconst transform = pipe(add5, multiply2, square)\nconsole.log('transform(3):', transform(3)) // (3+5)*2 = 16, 16^2 = 256\n\n// Works with string transformations too\nconst normalise = pipe(\n (s) => s.trim(),\n (s) => s.toLowerCase(),\n (s) => s.replace(/\\s+/g, '-'),\n)\nconsole.log('normalise result:', normalise(' Hello World ')) // 'hello-world'\n\n// Zero args returns the identity function\nconst id = pipe()\nconsole.log('identity:', id(42)) // 42",
|
|
88
|
-
"name": "pipe - Left-to-right function composition"
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
"id": "function-runAll",
|
|
92
|
-
"code": "import { runAll } from '@vielzeug/arsenal/function'\n\n// Run every teardown function — collect errors instead of stopping on first failure\nconst log = []\n\nconst teardowns = [\n () => { log.push('cleanup A'); },\n () => { log.push('cleanup B'); throw new Error('B failed'); },\n () => { log.push('cleanup C'); },\n]\n\ntry {\n runAll(teardowns, { reverse: true }) // LIFO order matches setup-teardown semantics\n} catch (err) {\n console.log('errors collected:', err instanceof AggregateError) // true\n console.log('error count:', err.errors.length) // 1\n console.log('still ran:', log) // ['cleanup C', 'cleanup B', 'cleanup A']\n}\n\n// Without failures — just runs all in order\nconst steps = []\nrunAll([() => steps.push(1), () => steps.push(2), () => steps.push(3)])\nconsole.log('steps:', steps) // [1, 2, 3]",
|
|
93
|
-
"name": "runAll - Run all callbacks, collect errors"
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
"id": "function-stash-async",
|
|
97
|
-
"code": "import { cache } from '@vielzeug/arsenal/cache'\n\nconst users = cache({ ttlMs: 5000 })\nlet fetchCount = 0\n\nfunction fetchUser(id) {\n return users.getOrLoad('user:' + id, async () => {\n fetchCount++\n await new Promise(resolve => setTimeout(resolve, 10))\n return { id, name: 'User ' + id }\n })\n}\n\nconst [first, second] = await Promise.all([fetchUser(1), fetchUser(1)])\nconsole.log('fetch count:', fetchCount)\nconsole.log('same reference:', first === second)\n\nusers.delete('user:1')\nconsole.log('fresh:', await fetchUser(1))",
|
|
98
|
-
"name": "cache - Async load deduplication"
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
"id": "function-throttle",
|
|
102
|
-
"code": "import { throttle } from '@vielzeug/arsenal/function'\n\nlet scrollCount = 0\nconst handleScroll = () => {\n scrollCount++\n console.log(`Scroll event #${scrollCount}`)\n}\n\nconst throttledScroll = throttle(handleScroll, 200)\n\n// Simulate rapid scroll events\nfor (let i = 0; i < 10; i++) {\n setTimeout(() => throttledScroll(), i * 50)\n}\n\nsetTimeout(() => {\n console.log('Total throttled calls:', scrollCount)\n}, 1000)",
|
|
103
|
-
"name": "throttle - Throttle function calls"
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
"id": "math-average",
|
|
107
|
-
"code": "import { average, median, sum } from '@vielzeug/arsenal/math'\n\nconst numbers = [10, 20, 30, 40, 50]\nconsole.log('Average:', average(numbers))\nconsole.log('Sum:', sum(numbers))\nconsole.log('Min:', Math.min(...numbers))\nconsole.log('Max:', Math.max(...numbers))\nconsole.log('Median:', median(numbers))",
|
|
108
|
-
"name": "average - Calculate average"
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
"id": "object-diff",
|
|
112
|
-
"code": "import { diff } from '@vielzeug/arsenal/object'\n\nconst before = {\n name: 'Alice',\n age: 25,\n email: 'alice@old.com',\n settings: { theme: 'light', lang: 'en' }\n}\n\nconst after = {\n name: 'Alice',\n age: 26,\n email: 'alice@new.com',\n settings: { theme: 'dark', lang: 'en' }\n}\n\nconst changes = diff(after, before)\nconsole.log('Changes detected:', changes)",
|
|
113
|
-
"name": "diff - Compare objects"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"id": "object-diffArrays",
|
|
117
|
-
"code": "import { diffArrays } from '@vielzeug/arsenal/object'\n\n// Default 'set' strategy — order independent\nconst v1 = [1, 2, 3]\nconst v2 = [2, 3, 4]\nconst setDiff = diffArrays(v1, v2)\nconsole.log('set diff:', setDiff) // { added: [4], removed: [1] }\n\n// 'lcs' strategy — ordered minimal diff\nconst before = [1, 2, 3, 4, 5]\nconst after = [1, 3, 4, 5, 6]\nconst lcsDiff = diffArrays(before, after, { strategy: 'lcs' })\nconsole.log('lcs diff:', lcsDiff) // { added: [6], removed: [2] }\n\n// With custom compareFn for objects\nconst oldUsers = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]\nconst newUsers = [{ id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]\nconst userDiff = diffArrays(oldUsers, newUsers, { compareFn: (a, b) => a.id === b.id })\nconsole.log('users:', userDiff) // { added: [{id:3,...}], removed: [{id:1,...}] }",
|
|
118
|
-
"name": "diffArrays - Set and LCS strategies"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
"id": "object-getPath",
|
|
122
|
-
"code": "import { getPath, getPathOr, requirePath } from '@vielzeug/arsenal/object'\n\nconst config = {\n server: { host: 'localhost', ports: [3000, 3001] },\n db: { name: 'mydb', pool: { min: 2, max: 10 } }\n}\n\n// Standard dot-notation\nconsole.log(getPath(config, 'server.host')) // 'localhost'\nconsole.log(getPath(config, 'db.pool.max')) // 10\nconsole.log(getPath(config, 'server.ports.0')) // 3000\nconsole.log(getPathOr(config, 'missing', 'default')) // 'default'\nconsole.log(getPath(config, 'server.ports[1]')) // 3001\n\ntry {\n requirePath(config, 'db.pool.timeout')\n} catch (e) {\n console.log('threw:', e.message)\n}\n\nconsole.log(getPathOr(config, '__proto__.polluted', 'safe')) // 'safe'",
|
|
123
|
-
"name": "getPath - Dot-notation access"
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
"id": "object-hash",
|
|
127
|
-
"code": "import { hash } from '@vielzeug/arsenal/object'\n\n// Stable cache key regardless of object key insertion order\nconst key1 = hash({ sort: 'asc', filter: { role: 'admin' } })\nconst key2 = hash({ filter: { role: 'admin' }, sort: 'asc' })\nconsole.log('Same key?', key1 === key2) // true\nconsole.log('Key:', key1) // '{\"filter\":{\"role\":\"admin\"},\"sort\":\"asc\"}'\n\n// Handles Date, RegExp, Set, Map, bigint\nconsole.log(hash(new Date('2024-01-01T00:00:00Z'))) // '[Date:2024-01-01T00:00:00.000Z]'\nconsole.log(hash(new Set([3, 1, 2]))) // '[Set:1,2,3]' — sorted\nconsole.log(hash(new Map([['b', 2], ['a', 1]]))) // '[Map:\"a\"=>1,\"b\"=>2]' — sorted\nconsole.log(hash(42n)) // '42n'\nconsole.log(hash(/foo/gi)) // '[RegExp:foo/gi]'\n\n// Circular references produce a sentinel — no stack overflow\nconst obj = { x: 1 }\nobj.self = obj\nconsole.log(hash(obj)) // '{\"self\":[Circular],\"x\":1}'\n\n// Class instances coerce to String(instance) by default\nclass Point {\n constructor(x, y) { this.x = x; this.y = y }\n toString() { return `Point(${this.x},${this.y})` }\n}\nconsole.log(hash(new Point(1, 2))) // 'Point(1,2)'",
|
|
128
|
-
"name": "hash - Deterministic cache key from any value"
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
"id": "object-merge",
|
|
132
|
-
"code": "import { deepMerge, shallowMerge } from '@vielzeug/arsenal/object'\n\nconst obj1 = { a: 1, b: { c: 2 }, d: [1, 2] }\nconst obj2 = { b: { d: 3 }, e: 4, d: [3, 4] }\nconst obj3 = { a: 10, f: 5 }\n\nconst deeplyMerged = deepMerge([obj1, obj2, obj3])\nconsole.log('Deep merge:', deeplyMerged)\n\nconst shallowlyMerged = shallowMerge(obj1, obj2, obj3)\nconsole.log('Shallow merge:', shallowlyMerged)\n\nconst config1 = {\n api: { baseUrl: 'https://api.dev', timeout: 5000 },\n features: { darkMode: true },\n}\nconst config2 = {\n api: { timeout: 10000, retries: 3 },\n features: { notifications: true },\n}\n\nconsole.log('Merged configs:', deepMerge([config1, config2]))",
|
|
133
|
-
"name": "deepMerge - Merge objects"
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"id": "object-parseJSON",
|
|
137
|
-
"code": "import { tryParseJson } from '@vielzeug/arsenal/object'\n\nconst valid = tryParseJson('{\"id\":1,\"name\":\"Alice\"}')\nconst invalid = tryParseJson('{')\n\nif (valid.ok) console.log('Parsed:', valid.value)\nif (!invalid.ok) console.log('Syntax error:', invalid.error.message)",
|
|
138
|
-
"name": "tryParseJson - Preserve JSON syntax errors"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
"id": "object-prune",
|
|
142
|
-
"code": "import { prune } from '@vielzeug/arsenal/object'\n\nconst data = {\n name: ' Alice ',\n age: 30,\n tags: ['js', null, '', 'ts', undefined],\n settings: { theme: 'dark', extra: null, empty: {} }\n}\n\nconst cleaned = prune(data)\nconsole.log('Pruned object:', cleaned)\n\n// Prune array\nconst mixed = [1, null, 2, undefined, '', 3]\nconsole.log('Pruned array:', prune(mixed))\n\n// Prune string\nconsole.log('Trimmed:', prune(' hello world '))\nconsole.log('Empty string:', prune(' ')) // undefined",
|
|
143
|
-
"name": "prune - Remove empty values"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
"id": "object-stash",
|
|
147
|
-
"code": "import { cache } from '@vielzeug/arsenal/cache'\n\nlet time = 0\nconst users = cache({ now: () => time, ttlMs: 5000 })\n\nusers.set('greeting', 'hello')\nconsole.log('get:', users.get('greeting'))\n\nconst loadUser = (id) => new Promise(resolve => setTimeout(() => resolve({ id, name: 'Alice' }), 50))\nconst [first, second] = await Promise.all([\n users.getOrLoad('user:1', () => loadUser(1)),\n users.getOrLoad('user:1', () => loadUser(1)),\n])\nconsole.log('same value:', first === second)\n\ntime = 5000\nconsole.log('expired:', users.get('greeting'))",
|
|
148
|
-
"name": "cache - Identity keys, TTL, and load deduplication"
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
"id": "string-camelcase",
|
|
152
|
-
"code": "import { camelCase, pascalCase, kebabCase, snakeCase } from '@vielzeug/arsenal/string'\n\nconst input = 'hello world example'\n\nconsole.log('camelCase:', camelCase(input))\nconsole.log('PascalCase:', pascalCase(input))\nconsole.log('kebab-case:', kebabCase(input))\nconsole.log('snake_case:', snakeCase(input))\n\n// Different input formats\nconst formats = [\n 'hello-world',\n 'hello_world',\n 'HelloWorld',\n 'helloWorld'\n]\n\nformats.forEach(str => {\n console.log(`\"${str}\" → camelCase: ${camelCase(str)}`)\n})",
|
|
153
|
-
"name": "camelCase - Convert to camelCase"
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
"id": "typed-is",
|
|
157
|
-
"code": "import { isDefined, isEmpty, isNil, isNumber, isPlainObject } from '@vielzeug/arsenal/guards'\n\nconst values = ['hello', 42, true, [1, 2, 3], {}, null, undefined]\n\nvalues.forEach(value => {\n console.log({\n array: Array.isArray(value),\n defined: isDefined(value),\n empty: isEmpty(value),\n nil: isNil(value),\n number: isNumber(value),\n plainObject: isPlainObject(value),\n string: typeof value === 'string',\n })\n})",
|
|
158
|
-
"name": "Guards and platform type checks"
|
|
159
|
-
}
|
|
160
|
-
],
|
|
161
|
-
"typeSignatures": {
|
|
162
|
-
"ArsenalError": "export class ArsenalError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is ArsenalError {\n return err instanceof ArsenalError;\n }\n}",
|
|
163
|
-
"ArsenalSerializationError": "export class ArsenalSerializationError extends ArsenalError {}",
|
|
164
|
-
"chunk": "export function chunk(input: string, size?: number): string[];\n\nexport function chunk<T>(input: T[], size?: number): T[][];\n\nexport function chunk<T>(input: T[] | string, size = 2): string[] | T[][] {\n if (!Array.isArray(input) && typeof input !== 'string')\n throw new TypeError('chunk: argument must be an array or string');\n\n if (!Number.isInteger(size) || size < 1) throw new RangeError('chunk: size must be a positive integer');\n\n return Array.from({ length: Math.ceil(input.length / size) }, (_, i) => input.slice(i * size, i * size + size)) as\n | string[]\n | T[][];\n}",
|
|
165
|
-
"filterMap": "export function filterMap<T, R>(array: T[], callback: (item: T, index: number, array: T[]) => R | undefined): R[] {\n const result: R[] = [];\n\n for (let index = 0; index < array.length; index++) {\n const value = callback(array[index], index, array);\n\n if (value !== undefined) {\n result.push(value);\n }\n }\n\n return result;\n}",
|
|
166
|
-
"groupBy": "export function groupBy<T>(array: T[], selector: (item: T) => Primitive): Record<string, T[]> {\n const result: Record<string, T[]> = {};\n\n for (const item of array) {\n const rawKey = selector(item);\n const key = rawKey === undefined || rawKey === null ? '_' : String(rawKey);\n\n if (isUnsafeKey(key)) continue;\n\n if (!result[key]) {\n result[key] = [];\n }\n\n result[key].push(item);\n }\n\n return result;\n}",
|
|
167
|
-
"indexBy": "export function indexBy<T>(array: T[], selector: (item: T) => Primitive): Record<string, T> {\n const result: Record<string, T> = {};\n\n for (const item of array) {\n const key = String(selector(item));\n\n if (isUnsafeKey(key)) continue;\n\n result[key] = item;\n }\n\n return result;\n}",
|
|
168
|
-
"partition": "export function partition<T>(array: T[], predicate: (item: T, index: number, array: T[]) => boolean): [T[], T[]] {\n const accepted: T[] = [];\n const rejected: T[] = [];\n\n for (let index = 0; index < array.length; index++) {\n if (predicate(array[index], index, array)) {\n accepted.push(array[index]);\n } else {\n rejected.push(array[index]);\n }\n }\n\n return [accepted, rejected];\n}",
|
|
169
|
-
"SortDirection": "export type SortDirection = 'asc' | 'desc';",
|
|
170
|
-
"SortSelectors": "export type SortSelectors<T> = Partial<Record<keyof T, SortDirection>>;",
|
|
171
|
-
"sort": "export function sort<T>(array: T[], selector: (item: T) => unknown, direction?: SortDirection): T[];\n\nexport function sort<T>(array: T[], selectors: SortSelectors<T>): T[];\n\nexport function sort<T>(\n array: T[],\n selectorOrSelectors: ((item: T) => unknown) | SortSelectors<T>,\n direction: SortDirection = 'asc',\n): T[] {\n if (!Array.isArray(array)) throw new TypeError('sort: first argument must be an array');\n\n if (typeof selectorOrSelectors === 'function') {\n const multiplier = direction === 'desc' ? -1 : 1;\n\n return [...array].sort((a, b) => compare(selectorOrSelectors(a), selectorOrSelectors(b)) * multiplier);\n }\n\n return [...array].sort(compareBy(selectorOrSelectors));\n}",
|
|
172
|
-
"uniq": "export function uniq<T>(array: T[], selector?: (item: T) => Primitive): T[] {\n if (array.length <= 1) {\n return [...array];\n }\n\n if (!selector) {\n return [...new Set(array)];\n }\n\n return dedupeBySelector(array, selector);\n}",
|
|
173
|
-
"AttemptResult": "export type AttemptResult<T> =\n | { ok: true; value: T }\n // eslint-disable-next-line perfectionist/sort-object-types\n | { ok: false; error: unknown };",
|
|
174
|
-
"isOk": "export function isOk<T>(result: AttemptResult<T>): result is { ok: true; value: T } {\n return result.ok === true;\n}",
|
|
175
|
-
"isFail": "export function isFail<T>(result: AttemptResult<T>): result is { ok: false; error: unknown } {\n return result.ok === false;\n}",
|
|
176
|
-
"attempt": "export async function attempt<T>(fn: () => T | Promise<T>): Promise<AttemptResult<T>> {\n try {\n return { ok: true, value: await fn() };\n } catch (error) {\n return { error, ok: false };\n }\n}",
|
|
177
|
-
"parallel": "export async function parallel<T, R>(\n array: readonly T[],\n callback: (item: T, index: number, array: readonly T[]) => Promise<R> | R,\n options: { abortOnError?: boolean; limit?: number; signal?: AbortSignal } = {},\n): Promise<R[]> {\n const { abortOnError = false, limit = Infinity, signal } = options;\n\n if (limit < 1) {\n throw new RangeError(`parallel: limit must be at least 1, got ${limit}`);\n }\n\n if (signal?.aborted) {\n throw abortError(signal);\n }\n\n if (array.length === 0) {\n return [];\n }\n\n const results: R[] = new Array(array.length);\n let currentIndex = 0;\n const failFastController = abortOnError ? new AbortController() : undefined;\n\n const worker = async (): Promise<void> => {\n while (true) {\n if (signal?.aborted) {\n throw abortError(signal);\n }\n\n if (failFastController?.signal.aborted) {\n return;\n }\n\n if (currentIndex >= array.length) {\n return;\n }\n\n const index = currentIndex++;\n\n try {\n results[index] = await callback(array[index]!, index, array);\n } catch (err) {\n failFastController?.abort();\n throw err;\n }\n }\n };\n\n const workerCount = Math.min(limit, array.length);\n const workers = Array.from({ length: workerCount }, () => worker());\n\n await Promise.all(workers);\n\n return results;\n}",
|
|
178
|
-
"RetryOptions": "export type RetryOptions = {\n delay?: number | ((attempt: number) => number);\n onError?: (error: unknown) => void;\n shouldRetry?: (error: unknown, attempt: number) => boolean;\n signal?: AbortSignal;\n timeout?: number;\n times?: number;\n};",
|
|
179
|
-
"retry": "export async function retry<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n { delay = 250, onError, shouldRetry, signal, timeout, times = 3 }: RetryOptions = {},\n): Promise<T> {\n if (!Number.isInteger(times) || times < 1) {\n throw new RangeError(`retry: times must be a positive integer, got ${times}`);\n }\n\n for (let tryCount = 1; tryCount <= times; tryCount++) {\n if (signal?.aborted) throw abortError(signal);\n\n const callSignal = buildSignal(timeout, signal);\n\n try {\n return await fn(callSignal);\n } catch (err) {\n if (tryCount === times) {\n onError?.(err);\n throw err;\n }\n\n if (shouldRetry && !shouldRetry(err, tryCount - 1)) {\n onError?.(err);\n throw err;\n }\n\n const ms = typeof delay === 'function' ? delay(tryCount - 1) : delay;\n\n if (ms > 0) await sleep(ms, signal);\n }\n }\n\n /* unreachable — the loop always returns or throws before exhausting all attempts */\n return undefined as never;\n}",
|
|
180
|
-
"sleep": "export async function sleep(timeout: number, signal?: AbortSignal): Promise<void> {\n if (!signal) {\n return new Promise((resolve) => setTimeout(resolve, timeout));\n }\n\n if (signal.aborted) {\n return Promise.reject(abortError(signal));\n }\n\n return new Promise<void>((resolve, reject) => {\n const id = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, timeout);\n\n const onAbort = () => {\n clearTimeout(id);\n signal.removeEventListener('abort', onAbort);\n reject(abortError(signal));\n };\n\n signal.addEventListener('abort', onAbort);\n });\n}",
|
|
181
|
-
"DebounceOptions": "export type DebounceOptions = {\n /** Invoke on the leading edge of the delay window. Default: `false`. */\n leading?: boolean;\n /** Invoke on the trailing edge of the delay window. Default: `true`. */\n trailing?: boolean;\n};",
|
|
182
|
-
"Debounced": "export type Debounced<T extends Fn> = ((...args: Parameters<T>) => ReturnType<T> | undefined) & {\n cancel(): void;\n flush(): ReturnType<T> | undefined;\n pending(): boolean;\n};",
|
|
183
|
-
"debounce": "export function debounce<T extends Fn>(\n fn: T,\n delay = 300,\n options: DebounceOptions = { leading: false, trailing: true },\n): Debounced<T> {\n const leading = options.leading ?? false;\n const trailing = options.trailing ?? true;\n\n if (!leading && !trailing) {\n warn('debounce: both `leading` and `trailing` are false — the wrapped function will never be invoked.');\n }\n\n let timerId: ReturnType<typeof setTimeout> | undefined;\n let lastArgs: Parameters<T> | undefined;\n let lastResult: ReturnType<T> | undefined;\n let leadingFired = false;\n\n const clearTimer = () => {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n timerId = undefined;\n }\n };\n\n const invokeTrailing = () => {\n clearTimer();\n leadingFired = false;\n\n if (!lastArgs) return undefined;\n\n const args = lastArgs;\n\n lastArgs = undefined;\n lastResult = fn(...args) as ReturnType<T>;\n\n return lastResult;\n };\n\n return Object.assign(\n (...args: Parameters<T>): ReturnType<T> | undefined => {\n lastArgs = args;\n\n if (leading && !leadingFired) {\n leadingFired = true;\n lastResult = fn(...args) as ReturnType<T>;\n // Clear args regardless of `trailing` — the leading call already consumed this\n // invocation. The trailing timer below only re-invokes `fn` if a *further* call\n // arrives during the cooldown window and repopulates `lastArgs`.\n lastArgs = undefined;\n\n if (!trailing) {\n // Leading-only: start cooldown — no trailing call needed\n timerId = setTimeout(() => {\n timerId = undefined;\n leadingFired = false;\n }, delay);\n\n return lastResult;\n }\n }\n\n // When leading-only, ignore calls within the cooldown window\n if (leading && !trailing && leadingFired) return lastResult;\n\n clearTimer();\n timerId = setTimeout(invokeTrailing, delay);\n\n return lastResult;\n },\n {\n cancel: () => {\n clearTimer();\n lastArgs = undefined;\n leadingFired = false;\n },\n flush: (): ReturnType<T> | undefined => invokeTrailing(),\n pending: () => timerId !== undefined,\n },\n );\n}",
|
|
184
|
-
"Once": "export type Once<T extends Fn> = T & { reset: () => void };",
|
|
185
|
-
"once": "export const once = <T extends Fn>(fn: T): Once<T> => {\n let result: ReturnType<T> | undefined;\n let called = false;\n\n const wrappedFn = ((...args: Parameters<T>): ReturnType<T> => {\n if (!called) {\n result = fn(...args) as ReturnType<T>;\n called = true;\n }\n\n return result as ReturnType<T>;\n }) as Once<T>;\n\n wrappedFn.reset = () => {\n result = undefined;\n called = false;\n };\n\n return wrappedFn;\n};",
|
|
186
|
-
"pipe": "export function pipe(): <T>(x: T) => T;\n\nexport function pipe<T extends readonly [Fn, ...Fn[]]>(...fns: T): (...args: FirstParameters<T>) => LastReturnType<T>;\n\nexport function pipe(...fns: Fn[]): Fn {\n if (fns.length === 0) return <T>(x: T): T => x;\n\n const [firstFn, ...restFns] = fns;\n\n return (...args: never[]) =>\n restFns.reduce(\n (prev: unknown, fn) => Reflect.apply(fn, undefined, [prev]),\n Reflect.apply(firstFn!, undefined, args),\n );\n}",
|
|
187
|
-
"ThrottleOptions": "export type ThrottleOptions = {\n leading?: boolean; // invoke at the start of the window\n trailing?: boolean; // invoke at the end with the last args\n};",
|
|
188
|
-
"Throttled": "export type Throttled<T extends Fn> = ((...args: Parameters<T>) => ReturnType<T> | undefined) & {\n cancel(): void;\n flush(): ReturnType<T> | undefined;\n pending(): boolean; // whether there's a pending call that flush() would execute\n};",
|
|
189
|
-
"throttle": "export function throttle<T extends Fn>(\n fn: T,\n delay = 700,\n options: ThrottleOptions = { leading: true, trailing: false },\n): Throttled<T> {\n const leading = options.leading ?? true;\n const trailing = options.trailing ?? false;\n\n let timerId: ReturnType<typeof setTimeout> | undefined;\n let lastInvokeTime = -Infinity;\n let lastArgs: Parameters<T> | undefined;\n let lastResult: ReturnType<T> | undefined;\n\n const clearTimer = () => {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n timerId = undefined;\n }\n };\n\n const scheduleTimer = (delayMs: number) => {\n timerId = setTimeout(timerExpired, delayMs);\n };\n\n const invoke = (now: number) => {\n lastInvokeTime = now;\n clearTimer();\n\n if (!lastArgs) return undefined;\n\n const args = lastArgs;\n\n lastArgs = undefined;\n lastResult = fn(...args) as ReturnType<T>;\n\n return lastResult;\n };\n\n const remaining = (now: number) => delay - (now - lastInvokeTime);\n\n const timerExpired = () => {\n const now = Date.now();\n\n if (lastArgs && remaining(now) <= 0) {\n // trailing edge invoke\n invoke(now);\n } else if (lastArgs) {\n // reschedule until a window elapses\n scheduleTimer(remaining(now));\n } else {\n clearTimer();\n }\n };\n\n return Object.assign(\n (...args: Parameters<T>): ReturnType<T> | undefined => {\n const now = Date.now();\n\n if (lastInvokeTime === -Infinity && !leading) {\n // If leading is false, start the window now but don't invoke immediately\n lastInvokeTime = now;\n }\n\n lastArgs = args;\n\n const rem = remaining(now);\n\n if (rem <= 0) {\n // Window elapsed: invoke now\n return invoke(now);\n } else if (trailing && !timerId) {\n // Schedule trailing call if not already scheduled\n scheduleTimer(rem);\n }\n\n return lastResult;\n },\n {\n cancel: () => {\n clearTimer();\n lastArgs = undefined;\n lastInvokeTime = -Infinity;\n },\n flush: (): ReturnType<T> | undefined => {\n if (!lastArgs) return undefined;\n\n return invoke(Date.now());\n },\n // Pending if a trailing call is scheduled OR there are queued args.\n pending: () => lastArgs !== undefined || timerId !== undefined,\n },\n );\n}",
|
|
190
|
-
"allOf": "export function allOf<T>(...predicates: Predicate<T>[]): Predicate<T> {\n return (value) => predicates.every((predicate) => predicate(value));\n}",
|
|
191
|
-
"anyOf": "export function anyOf<T>(...predicates: Predicate<T>[]): Predicate<T> {\n return (value) => predicates.some((predicate) => predicate(value));\n}",
|
|
192
|
-
"noneOf": "export function noneOf<T>(...predicates: Predicate<T>[]): Predicate<T> {\n return (value) => predicates.every((predicate) => !predicate(value));\n}",
|
|
193
|
-
"not": "export function not<T>(predicate: Predicate<T>): Predicate<T> {\n return (value) => !predicate(value);\n}",
|
|
194
|
-
"isDefined": "export function isDefined<T>(arg: T | undefined): arg is T {\n return arg !== undefined;\n}",
|
|
195
|
-
"isEqual": "export function isEqual(a: unknown, b: unknown): boolean {\n return safeIsEqual(a, b, new WeakMap());\n}",
|
|
196
|
-
"isNil": "export function isNil(arg: unknown): arg is null | undefined {\n return arg === undefined || arg === null;\n}",
|
|
197
|
-
"isPlainObject": "export function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value == null || typeof value !== 'object') return false;\n\n const prototype = Object.getPrototypeOf(value);\n\n return prototype === Object.prototype || prototype === null;\n}",
|
|
198
|
-
"clamp": "export function clamp(n: number, min?: number, max?: number): number {\n if (min != null && n < min) return min;\n\n if (max != null && n > max) return max;\n\n return n;\n}",
|
|
199
|
-
"range": "export function range(stop: number): number[];\n\nexport function range(start: number, stop: number): number[];\n\nexport function range(start: number, stop: number, step: number): number[];\n\nexport function range(startOrStop: number, stop?: number, step?: number): number[] {\n const [resolvedStart, resolvedStop, resolvedStep] =\n stop === undefined ? [0, startOrStop, 1] : step === undefined ? [startOrStop, stop, 1] : [startOrStop, stop, step];\n\n if (!Number.isFinite(resolvedStart) || !Number.isFinite(resolvedStop) || !Number.isFinite(resolvedStep)) {\n throw new RangeError('range: start, stop, and step must be finite numbers');\n }\n\n if (resolvedStep === 0) throw new RangeError('range: step cannot be 0');\n\n if (resolvedStart === resolvedStop) {\n return [];\n }\n\n const raw = (resolvedStop - resolvedStart) / resolvedStep;\n const length = Math.max(0, Number.isInteger(raw) ? raw : Math.ceil(raw + Number.EPSILON));\n\n if (length > 10_000_000) throw new RangeError('range: exceeds maximum allowed size of 10,000,000');\n\n return Array.from({ length }, (_, i) => resolvedStart + i * resolvedStep);\n}",
|
|
200
|
-
"getPath": "export function getPath<T extends Obj, P extends string>(item: T, path: P): PathValue<T, P> | undefined {\n return readPath(item, path) as PathValue<T, P> | undefined;\n}",
|
|
201
|
-
"getPathOr": "export function getPathOr<T extends Obj, P extends string, F>(item: T, path: P, fallback: F): PathValue<T, P> | F {\n const value = readPath(item, path);\n\n return (value === undefined ? fallback : value) as PathValue<T, P> | F;\n}",
|
|
202
|
-
"requirePath": "export function requirePath<T extends Obj, P extends string>(item: T, path: P): Exclude<PathValue<T, P>, undefined> {\n const value = readPath(item, path);\n\n if (value === undefined) throw new TypeError(`Path does not exist: '${path}'`);\n\n return value as Exclude<PathValue<T, P>, undefined>;\n}",
|
|
203
|
-
"HashOptions": "export type HashOptions = {\n /**\n * What to do when a class instance is encountered (an object whose prototype is\n * neither `Object.prototype` nor `null`).\n *\n * - `'coerce'` (default) — calls `String(value)`.\n * - `'throw'` — throws an `ArsenalSerializationError`.\n */\n onClassInstance?: 'coerce' | 'throw';\n};",
|
|
204
|
-
"hash": "export function hash(value: unknown, options?: HashOptions): string {\n return _hash(value, options, new Set<object>());\n}",
|
|
205
|
-
"omit": "export function omit<T extends Obj, K extends keyof T>(obj: T, omittedKeys: readonly K[]): Omit<T, K> {\n const blacklist = new Set<PropertyKey>(omittedKeys as readonly PropertyKey[]);\n const out = {} as Omit<T, K>;\n\n for (const key of Object.keys(obj) as Array<keyof T>) {\n if (isUnsafeKey(key as PropertyKey)) continue;\n\n if (!blacklist.has(key)) {\n (out as T)[key] = obj[key];\n }\n }\n\n return out;\n}",
|
|
206
|
-
"pick": "export function pick<T extends Obj, K extends keyof T>(obj: T, selectedKeys: readonly K[]): Pick<T, K> {\n const out = {} as Pick<T, K>;\n\n for (const key of selectedKeys) {\n if (isUnsafeKey(key as PropertyKey)) continue;\n\n if (Object.hasOwn(obj, key)) {\n out[key] = obj[key];\n }\n }\n\n return out;\n}",
|
|
207
|
-
"uuid": "export function uuid(): string {\n return crypto.randomUUID();\n}",
|
|
208
|
-
"camelCase": "export function camelCase(str: string): string {\n return normalizeCase(str, ' ')\n .replace(/[^\\p{L}\\p{N}]+(.)/gu, (_, char) => char.toUpperCase())\n .replace(/^./, (char) => char.toLowerCase());\n}"
|
|
209
|
-
}
|
|
210
|
-
}
|
package/data/packages/assay.json
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export { AssayError, AssayQueryError, AssayTimeoutError } from './errors';\n\nexport {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';\nexport { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';\nexport {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Assay — Framework-agnostic DOM testing primitives\ndescription: Scoped DOM queries, exact event dispatch, and cancellable async waiting for browser tests.\npackage: assay\ncategory: testing\nkeywords: [testing, dom, events, queries, custom-elements]\nrelated: [ore, refine]\nexports:\n [\n within,\n queryInShadow,\n queryAllInShadow,\n queryPart,\n getSlotted,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n waitUntil,\n retry,\n waitForEvent,\n delay,\n nextTick,\n AssayError,\n AssayQueryError,\n AssayTimeoutError,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"assay\" />\n\n## Why Assay?\n\nAssay provides focused DOM test primitives that work with vanilla elements, custom elements, and framework-rendered output. It scopes queries, dispatches exact browser event classes, and waits on explicit conditions without imposing a renderer or browser automation stack.\n\n```ts\n// Before\nbutton.dispatchEvent(new MouseEvent('click', { bubbles: true }));\nawait new Promise((resolve) => setTimeout(resolve, 100));\n\n// After\nfireClick(view.get('button.submit'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n| Feature | Assay | Testing Library DOM | Browser automation |\n| ------------------- | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- |\n| Bundle size | <PackageInfo package=\"assay\" type=\"size\" /> | Larger query layer | Browser runtime required |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Scoped DOM queries | `within()` and shadow helpers | Renderer-oriented queries | Manual selectors |\n| Deterministic waits | `waitUntil()` and `waitForEvent()` | Framework-dependent | Full browser timing |\n\n<div class=\"decision-callout\">\n\n**Use Assay when** a DOM unit test needs readable queries, dispatched events, or a bounded async wait without adopting a rendering framework.\n\n**Consider browser integration tests when** correctness depends on browser default actions, focus behavior, pointer capture, layout, or accessibility-tree behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add -D @vielzeug/assay\n```\n\n```sh [npm]\nnpm install -D @vielzeug/assay\n```\n\n```sh [yarn]\nyarn add -D @vielzeug/assay\n```\n\n:::\n\n## Quick Start\n\nScope a test fixture, dispatch an event, and wait for resulting DOM state.\n\n```ts\nimport { fireClick, waitUntil, within } from '@vielzeug/assay';\n\nconst panel = document.createElement('section');\npanel.innerHTML = '<button>Save</button><output></output>';\npanel.querySelector('button')!.addEventListener('click', () => {\n panel.querySelector('output')!.textContent = 'Saved';\n});\n\nconst view = within(panel);\nfireClick(view.get('button'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `within(root)` scopes nullable and required DOM queries.\n- `queryInShadow`, `queryPart`, and `getSlotted` cross custom-element boundaries explicitly.\n- `fireClick`, `fireInput`, `fireKeyDown`, and peers dispatch exact synchronous events.\n- `waitUntil`, `retry`, and `waitForEvent` provide bounded, abortable async waiting.\n- `delay` and `nextTick` model explicit timer and microtask scheduling.\n- `AssayError`, `AssayQueryError`, and `AssayTimeoutError` provide typed failures.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — component authoring and test fixtures that pair with Assay DOM helpers.\n- [Refine](/refine/) — accessible components with component-specific test assertions.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Assay — API Reference\ndescription: API reference for @vielzeug/assay queries, event dispatch, and async waiting.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------------------------------- | ------------------------------------------- | -------------- | ------------------------------------------------ |\n| `within` | Creates scoped query API | Sync | Required `get*` methods throw `AssayQueryError` |\n| `queryInShadow` / `queryPart` / `getSlotted` | Crosses custom-element boundaries | Sync | Open shadow roots are required |\n| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |\n| `waitUntil` / `retry` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |\n| `delay` / `nextTick` | Schedules timers or microtasks | Async | Prefer `nextTick()` for microtask-scheduled work |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ----------------- | ---------------------------------------------------- |\n| `@vielzeug/assay` | DOM queries, events, wait helpers, errors, and types |\n\n## Queries\n\n### `within(root)`\n\nCreates a `QueryScope` for an `Element`, `ShadowRoot`, `Document`, or `DocumentFragment`.\n\n| Method | Returns | Use |\n| --------------------------------- | ----------------- | -------------------------------------------- |\n| `get(selector)` | `Element` | Required CSS match; throws `AssayQueryError` |\n| `query(selector)` | `Element \\| null` | Optional CSS match |\n| `queryAll(selector)` | `Element[]` | All CSS matches |\n| `getByText(text, selector?)` | `Element` | Required exact trimmed-text match |\n| `queryByText(text, selector?)` | `Element \\| null` | Optional exact trimmed-text match |\n| `queryAllByText(text, selector?)` | `Element[]` | All exact trimmed-text matches |\n| `getByTestId(id)` | `Element` | Required `data-testid` match |\n| `queryByTestId(id)` | `Element \\| null` | Optional `data-testid` match |\n| `queryAllByTestId(id)` | `Element[]` | All `data-testid` matches |\n\nText selectors default to `'*'`. Required-query failures include the lookup and a bounded view of the scoped DOM.\n\n### Shadow and slot helpers\n\n| Function | Returns | Description |\n| ---------------------------------- | ----------------- | ---------------------------------------------------- |\n| `queryInShadow(host, selector)` | `Element \\| null` | First match in an open shadow root |\n| `queryAllInShadow(host, selector)` | `Element[]` | All matches in an open shadow root |\n| `queryPart(host, part)` | `Element \\| null` | First shadow element whose `part` token matches |\n| `getSlotted(host, slotName?)` | `Element[]` | Direct light-DOM children in a named or default slot |\n\nThese helpers return `null` or `[]` when there is no shadow root. Dynamic test IDs, parts, and slot names are matched\nas attribute values rather than interpolated into CSS selectors.\n\n## Event dispatch\n\nAll event helpers synchronously return `dispatchEvent()`'s boolean result.\n\n```ts\nimport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from '@vielzeug/assay';\n\nfireClick(button, { clientX: 20 });\nfireInput(input);\nfireKeyDown(input, { key: 'Enter' });\nfireCustom(element, { detail: { id: '42' }, type: 'item-added' });\ndispatch(element, new Event('ready'));\n```\n\n| Function | Event class | Defaults |\n| --------------------------- | --------------- | ------------------------------------------------------ |\n| `fireBlur` / `fireFocus` | `FocusEvent` | Platform defaults (`bubbles: false`) |\n| `fireChange` | `Event` | `bubbles: true` |\n| `fireInput` | `InputEvent` | `bubbles: true` |\n| `fireClick` | `MouseEvent` | `bubbles: true`, `cancelable: true` |\n| `fireKeyDown` / `fireKeyUp` | `KeyboardEvent` | `bubbles: true`, `cancelable: true` |\n| `fireSubmit` | `SubmitEvent` | `bubbles: true`, `cancelable: true` |\n| `fireCustom` | `CustomEvent` | `bubbles: true`, `cancelable: true`, `composed: false` |\n\n`fireCustom(target, { type, ...init })` requires the event type in its options object. Assay intentionally does not\nprovide browser-default or fallback pointer/touch simulation.\n\n## Async waiting\n\n```ts\nawait waitUntil(() => ready, { interval: 20, signal, timeout: 1000 });\nawait retry(() => expect(spy).toHaveBeenCalled(), { signal, timeout: 1000 });\nawait waitForEvent(target, 'ready', { signal, timeout: 1000 });\nawait delay(100, { signal });\nawait nextTick();\n```\n\n| Function | Success condition | Options |\n| -------------------------------------- | ------------------------ | ------------------------------------------ |\n| `waitUntil(predicate, options?)` | Predicate returns `true` | `timeout`, `interval`, `signal` |\n| `retry(assertion, options?)` | Assertion stops throwing | `timeout`, `interval`, `signal`, `message` |\n| `waitForEvent(target, type, options?)` | Target emits `type` | `timeout`, `signal` |\n| `delay(ms?, options?)` | Timer elapses | `signal` |\n| `nextTick()` | Next microtask | none |\n\n`waitUntil`, `retry`, and `waitForEvent` reject with `AssayTimeoutError` when their timeout expires. A supplied abort\nsignal rejects with its reason and removes timers and event listeners.\n\n## Types\n\n```ts\nexport interface QueryScope {\n get(selector: string): Element;\n query(selector: string): Element | null;\n queryAll(selector: string): Element[];\n getByText(text: string, selector?: string): Element;\n queryByText(text: string, selector?: string): Element | null;\n queryAllByText(text: string, selector?: string): Element[];\n getByTestId(id: string): Element;\n queryByTestId(id: string): Element | null;\n queryAllByTestId(id: string): Element[];\n}\n```\n\n`CustomEventOptions`, `DelayOptions`, `RetryOptions`, and `WaitOptions` are exported option types for event and wait helpers.\n\n## Errors\n\n| Error | Meaning |\n| ------------------- | -------------------------------------- |\n| `AssayError` | Base class for Assay-originated errors |\n| `AssayQueryError` | A required `get*` query had no match |\n| `AssayTimeoutError` | A wait operation reached its timeout |\n\n`AssayError.is(value)` narrows any value to the Assay error hierarchy.\n",
|
|
6
|
-
"usage": "---\ntitle: Assay — Usage Guide\ndescription: Scoped DOM queries, exact event dispatch, and cancellable waiting with @vielzeug/assay.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`within(root)` is Assay's query API. It accepts an element, document fragment, or shadow root and keeps every lookup\nin that scope. Use `get*` when a match is required and nullable `query*` methods when absence is part of the assertion.\n\n```ts\nimport { within } from '@vielzeug/assay';\n\nconst view = within(panel.shadowRoot!);\n\nconst save = view.get<HTMLButtonElement>('button.save');\nconst status = view.queryByText('Saved');\n\nexpect(view.query('.error')).toBeNull();\nexpect(view.getByTestId('summary').textContent).toContain('Complete');\n```\n\n`get()`, `getByText()`, and `getByTestId()` throw `AssayQueryError` with the lookup and a bounded rendering of the\nscoped DOM. This keeps a failed required lookup diagnosable without non-null assertions.\n\nUse the cross-boundary helpers for custom elements:\n\n```ts\nimport { getSlotted, queryAllInShadow, queryPart } from '@vielzeug/assay';\n\nconst trigger = queryPart(menu, 'trigger');\nconst options = queryAllInShadow(menu, '[role=\"option\"]');\nconst footerActions = getSlotted(dialog, 'footer');\n```\n\n## Exact event dispatch\n\nAssay dispatches the platform event class named by each helper. It does not model browser activation, focus\nmanagement, or form defaults. `fireFocus()` and `fireBlur()` use their non-bubbling platform defaults; use\n`focusin`/`focusout` events when testing delegated focus listeners. Use `element.click()` when native activation is\nthe behavior under test; use Assay when testing an event listener or a controlled state transition.\n\n```ts\nimport { fireClick, fireCustom, fireInput, fireKeyDown } from '@vielzeug/assay';\n\ninput.value = 'Ada';\nfireInput(input);\n\nfireKeyDown(input, { key: 'Enter' });\nfireClick(saveButton);\nfireCustom(panel, { detail: { value: 42 }, type: 'value-change' });\n```\n\nEvery helper returns `dispatchEvent()`'s boolean result. `dispatch(target, event)` is available when an existing\nevent instance is the clearest expression of the test.\n\n## Waiting\n\nChoose the waiting primitive by the test's assertion shape:\n\n```ts\nimport { delay, retry, waitForEvent, waitUntil } from '@vielzeug/assay';\n\nawait waitUntil(() => panel.querySelector('.status')?.textContent === 'Ready');\n\nawait retry(() => {\n expect(onSave).toHaveBeenCalledOnce();\n});\n\nconst completed = waitForEvent<CustomEvent<{ id: string }>>(panel, 'save-complete', {\n signal: AbortSignal.timeout(1000),\n});\nfireClick(saveButton);\nexpect((await completed).detail.id).toBeDefined();\n\nawait delay(100); // real timer dependency, such as a debounce\n```\n\n`waitUntil()` retries only a boolean predicate. `retry()` retries only a callback that throws until it succeeds.\nBoth, and `waitForEvent()`, accept `timeout` and `signal`; `waitUntil()` and `retry()` also accept `interval`.\nTimeouts reject with `AssayTimeoutError`; aborts reject with the signal's reason.\n\n`nextTick()` resolves after one microtask. Prefer it for microtask-scheduled reactive work over a timer delay.\n\n## Testing custom elements\n\nUse Ore for component mounting and Assay for generic DOM concerns:\n\n```ts\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { mount } from '@vielzeug/ore/testing';\n\nconst fixture = await mount(\n () => html`\n <button @click=${onSave}>Save</button>\n `,\n);\n\nfireClick(fixture.get('button'));\nawait fixture.flush();\n\nexpect(onSave).toHaveBeenCalledOnce();\n```\n\nRefine's testing entry point contains only Refine-specific assertions and typed mount wrappers. Import Assay helpers\ndirectly instead of routing generic DOM operations through another package.\n\n## Best Practices\n\n- Scope multiple assertions with `within()` rather than repeatedly querying the document.\n- Prefer `get*` for required controls and `query*` for intentional absence checks.\n- Make form state and event boundaries explicit: assign `.value`, then call `fireInput()` or `fireChange()`.\n- Use browser integration tests for focus, disabled activation, pointer capture, and other browser-default behavior.\n- Use `waitForEvent()` for an emitted event, `waitUntil()` for a condition, and `retry()` for assertions.\n",
|
|
7
|
-
"examples": "---\ntitle: Assay — Examples\ndescription: Practical examples and recipes for assay.\n---\n\n## Examples\n\n- [Custom Element Interaction](./examples/custom-element-interaction.md)\n- [Waiting for Async Updates](./examples/waiting-for-async-updates.md)\n"
|
|
8
|
-
},
|
|
9
|
-
"examples": [],
|
|
10
|
-
"typeSignatures": {
|
|
11
|
-
"AssayError": "export { AssayError, AssayQueryError, AssayTimeoutError } from './errors';",
|
|
12
|
-
"AssayQueryError": "export { AssayError, AssayQueryError, AssayTimeoutError } from './errors';",
|
|
13
|
-
"AssayTimeoutError": "export { AssayError, AssayQueryError, AssayTimeoutError } from './errors';",
|
|
14
|
-
"CustomEventOptions": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
15
|
-
"dispatch": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
16
|
-
"fireBlur": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
17
|
-
"fireChange": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
18
|
-
"fireClick": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
19
|
-
"fireCustom": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
20
|
-
"fireFocus": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
21
|
-
"fireInput": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
22
|
-
"fireKeyDown": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
23
|
-
"fireKeyUp": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
24
|
-
"fireSubmit": "export {\n type CustomEventOptions,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
|
|
25
|
-
"getSlotted": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
|
|
26
|
-
"QueryScope": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
|
|
27
|
-
"queryAllInShadow": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
|
|
28
|
-
"queryInShadow": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
|
|
29
|
-
"queryPart": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
|
|
30
|
-
"within": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
|
|
31
|
-
"DelayOptions": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
32
|
-
"delay": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
33
|
-
"nextTick": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
34
|
-
"RetryOptions": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
35
|
-
"retry": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
36
|
-
"WaitOptions": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
37
|
-
"waitForEvent": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';",
|
|
38
|
-
"waitUntil": "export {\n type DelayOptions,\n delay,\n nextTick,\n type RetryOptions,\n retry,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';"
|
|
39
|
-
}
|
|
40
|
-
}
|