first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,87 @@
# debug
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `debug` module provides a lightweight, injectable debug logging utility that deduplicates repeated messages using a circular buffer. Unlike `console.log`, which emits every call unconditionally, `createDebug` tracks recently seen messages and suppresses duplicates until they age out of the buffer. This makes it practical to place debug statements in hot paths without flooding logs.
The module is deliberately free of OpenTelemetry instrumentation — it is itself an instrumentation primitive and instrumenting it would create infinite recursion.
## Features
- Circular ring buffer with configurable capacity (`bufferSize`, default 10)
- Automatic eviction of the oldest entry when buffer is full
- Keyed by `JSON.stringify` of all arguments; `Error` objects are keyed by their `.message`
- Injectable log/warn/error/info functions for testing without mocking
- Module-level `debug` singleton for drop-in use
- Supports multi-argument log calls (each unique combination is tracked)
- Zero external dependencies
## Usage: Basic Example
```ts
import { debug } from "@suaveplan/utils/debug";
// Only logs once even if called many times with the same args
debug.log("Connecting to database...");
debug.log("Connecting to database..."); // suppressed
debug.warn("Low memory");
```
## Usage: Advanced Example
```ts
import { createDebug } from "@suaveplan/utils/debug";
// Custom buffer and injectable log function for testing
const captured: unknown[][] = [];
const dbg = createDebug({
bufferSize: 3,
log: (...args) => captured.push(args),
});
dbg.log("a"); // logged
dbg.log("a"); // suppressed
dbg.log("b"); // logged
dbg.log("c"); // logged — buffer now [a, b, c]
dbg.log("d"); // logged — evicts "a"
dbg.log("a"); // logged — "a" was evicted, so fresh
console.log(captured.length); // 5
```
## API Reference
### `createDebug(options?: DebugOptions): DebugInstance`
Creates a new debug instance with an independent circular buffer.
**Parameters:**
- `options.log` — `(...args: unknown[]) => void` — custom log function (default: `console.log`)
- `options.warn` — `(...args: unknown[]) => void` — custom warn function (default: `console.warn`)
- `options.error` — `(...args: unknown[]) => void` — custom error function (default: `console.error`)
- `options.info` — `(...args: unknown[]) => void` — custom info function (default: `console.info`)
- `options.bufferSize` — `number` — maximum remembered messages before eviction (default: `10`)
**Returns:** `DebugInstance` with `.log`, `.warn`, `.error`, `.info` methods.
### `debug: DebugInstance`
Module-level singleton created with default options. Suitable for direct import and use without configuration.
### `DebugOptions`
Interface defining injectable overrides for the debug factory.
### `DebugInstance`
Interface returned by `createDebug` with `log`, `warn`, `error`, `info` methods.
## Implementation Notes
The buffer is implemented as a fixed-size ring array with a head pointer. Adding a new message writes to `head % maxSize` and advances `head`. Reading iterates backward from head across `size` entries. This gives O(n) lookup — acceptable for the default size of 10, which is always tiny. Message identity is computed via `JSON.stringify` after normalizing `Error` instances to their `.message` string, so `new Error("boom")` repeated is treated as a duplicate.
Each `createDebug` call creates an independent buffer. The module-level `debug` singleton shares no state with other instances, making instances safe to use in parallel.

View File

@@ -0,0 +1,94 @@
# email
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `email` module provides a provider-aware email normalization utility that converts raw email addresses into their canonical form. Different email providers apply different rules for aliases, dots, and sub-addressing — Gmail ignores dots and uses `+` aliasing, Yahoo uses `-` aliasing, Microsoft uses `+` aliasing. By applying provider-specific canonicalization, the module enables reliable duplicate detection, login normalization, and spam-prevention heuristics across a diverse user base.
## Features
- **Gmail / Googlemail canonicalization**: strips all dots from the local part and removes `+alias` suffixes, so `t.e.s.t+tag@gmail.com` → `test@gmail.com`.
- **Yahoo canonicalization**: strips `-alias` suffixes while preserving dots, so `user-tag@yahoo.com` → `user@yahoo.com`.
- **Microsoft (Outlook / Hotmail / Live) canonicalization**: strips `+alias` suffixes while preserving dots.
- **Generic-domain canonicalization**: strips `+alias` suffix and removes all dots from the local part for any domain not in the known-provider lists.
- **Graceful malformed-input handling**: inputs without an `@` character or with an empty local part after stripping are returned unchanged (lowercased), so the function never throws.
- **Zod schemas**: `emailSchema` and `normalizedEmailSchema` are exported for schema-driven validation pipelines.
## Usage: Basic Example
```typescript
import { normalizeEmail } from "@suaveplan/utils/email";
// Gmail: dots ignored, plus alias stripped
normalizeEmail("John.Doe+newsletter@gmail.com");
// → "johndoe@gmail.com"
// Yahoo: dash alias stripped, dots kept
normalizeEmail("jane.smith-promo@yahoo.com");
// → "jane.smith@yahoo.com"
```
## Usage: Advanced Example
```typescript
import { normalizeEmail } from "@suaveplan/utils/email";
// De-duplicate user registrations
function deduplicateUsers(emails: string[]): string[] {
const seen = new Set<string>();
const unique: string[] = [];
for (const email of emails) {
const canonical = normalizeEmail(email);
if (!seen.has(canonical)) {
seen.add(canonical);
unique.push(email);
}
}
return unique;
}
const raw = [
"alice@gmail.com",
"a.l.i.c.e@gmail.com",
"alice+spam@gmail.com",
"bob@yahoo.com",
"bob-promo@yahoo.com",
];
deduplicateUsers(raw);
// → ["alice@gmail.com", "bob@yahoo.com"]
```
## API Reference
### `normalizeEmail(email: string): string`
Normalize an email address by applying provider-specific canonicalization rules.
| Parameter | Type | Description |
|-----------|----------|--------------------------------------|
| `email` | `string` | Raw email address to normalize. |
**Returns:** `string` — the canonicalized email address, always lowercased.
**Does not throw.** Malformed inputs (no `@`, empty parts) are returned as-is lowercased.
### `emailSchema`
Zod schema (`z.string().email()`) for validating that a value is a well-formed email address.
### `normalizedEmailSchema`
Zod schema (`z.string().min(1)`) for the output of `normalizeEmail`.
## Implementation Notes
The provider lookup uses three static `const` arrays (`GOOGLE_DOMAINS`, `YAHOO_DOMAINS`, `MICROSOFT_DOMAINS`) that cover the most common hosted-email providers. `Array.includes()` is O(n) but the arrays are tiny (2–3 elements each), making the cost negligible vs. a Map lookup.
The local-part stripping uses a single `split(/[+-]/)[0]` to find the first `+` or `-` delimiter. This is intentionally conservative: it does not try to validate full RFC 5321 local-part syntax, only to strip the most common alias prefixes.
The function never modifies the input domain. Domain normalization (punycode, trailing-dot removal) is outside the scope of this utility.
Known limitation: provider lists are hardcoded. Organizations with custom Google Workspace or Microsoft 365 domains are not recognized as such; they follow the generic path. A future version could accept a custom provider-config option.

View File

@@ -0,0 +1,83 @@
# errors/utils-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `utils-error` module defines the typed error class and stable error-code registry for the entire `@suaveplan/utils` package. Every runtime error produced by the package is an instance of `UtilsError`, which extends `SuaveplanError` from `@suaveplan/error`. This gives consumers a single `instanceof` check plus a stable, machine-readable `code` string to branch on — far more reliable than parsing error messages, which may change between releases.
## Features
- **`UTILS_CODES`**: a registry of all error codes in the package, defined with `defineCodes("UTILS", [...])` from `@suaveplan/error`. Each code is prefixed `UTILS/` and namespaced by subsystem (e.g. `UTILS/OBJECT.PROTOTYPE_POLLUTION`, `UTILS/SOUND.NO_VARIATIONS`).
- **`UtilsError`**: typed subclass of `SuaveplanError<UtilsCode>`. Carries `.code`, `.message`, and optional `SuaveplanErrorOptions` (cause, context, redact). The constructor visibility is `public` to allow direct instantiation anywhere in the package.
- **`UtilsCode`**: TypeScript type alias for the union of all values in `UTILS_CODES`, enabling exhaustive `switch` statements and narrowed error handling.
## Usage: Basic Example
```typescript
import { UTILS_CODES, UtilsError } from "@suaveplan/utils/errors";
throw new UtilsError(
UTILS_CODES["VALIDATION.SIZE_EXCEEDED"],
"Payload is too large",
);
```
## Usage: Advanced Example
```typescript
import { UTILS_CODES, UtilsError, type UtilsCode } from "@suaveplan/utils/errors";
function handleError(err: unknown): void {
if (!(err instanceof UtilsError)) throw err;
const code: UtilsCode = err.code;
switch (code) {
case UTILS_CODES["VALIDATION.SIZE_EXCEEDED"]:
console.error("Payload too large:", err.message);
break;
case UTILS_CODES["SOUND.NO_VARIATIONS"]:
console.warn("Audio not found:", err.message);
break;
default:
throw err; // re-throw unexpected utils errors
}
}
```
## API Reference
### `UTILS_CODES`
Object mapping dot-path keys to fully-qualified error code strings.
| Key | Code |
|-----|------|
| `"OBJECT.PROTOTYPE_POLLUTION"` | `"UTILS/OBJECT.PROTOTYPE_POLLUTION"` |
| `"OBJECT.CIRCULAR"` | `"UTILS/OBJECT.CIRCULAR"` |
| `"NUMBER.OUT_OF_RANGE"` | `"UTILS/NUMBER.OUT_OF_RANGE"` |
| `"RANDOM.EMPTY_ARRAY"` | `"UTILS/RANDOM.EMPTY_ARRAY"` |
| `"RANDOM.INDEX_OUT_OF_BOUNDS"` | `"UTILS/RANDOM.INDEX_OUT_OF_BOUNDS"` |
| `"VALIDATION.SIZE_EXCEEDED"` | `"UTILS/VALIDATION.SIZE_EXCEEDED"` |
| `"VALIDATION.PARSE_FAILED"` | `"UTILS/VALIDATION.PARSE_FAILED"` |
| `"PLATFORM.UNAVAILABLE"` | `"UTILS/PLATFORM.UNAVAILABLE"` |
| `"SOUND.NO_VARIATIONS"` | `"UTILS/SOUND.NO_VARIATIONS"` |
### `class UtilsError extends SuaveplanError<UtilsCode>`
| Parameter | Type | Description |
|-----------|------|-------------|
| `code` | `UtilsCode` | Stable error code from `UTILS_CODES`. |
| `message` | `string` | Human-readable description. |
| `options` | `SuaveplanErrorOptions?` | Optional cause, context, redact fields. |
### `type UtilsCode`
Union type of all string values in `UTILS_CODES`.
## Implementation Notes
`defineCodes` is a helper from `@suaveplan/error` that prefixes each code with the registry namespace (`"UTILS"`) and freezes the result. This ensures codes are immutable at runtime and catch typos at TypeScript compile time through the `as const` assertion on the code array.
The `public constructor` visibility is explicit (and annotated with a biome-ignore comment) to satisfy the linter's `noUselessConstructor` rule while documenting the intentional widening from the `protected` default inherited from `SuaveplanError`.

View File

@@ -0,0 +1,134 @@
# number
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `number` module provides IEEE-754 float encoding and decoding utilities for Float16, Float32, and Float64 formats. JavaScript's native `number` type is always Float64, but many binary protocols, graphics APIs, and machine-learning frameworks use the more compact Float16 or Float32 representations. This module allows lossless round-tripping between JavaScript numbers and their binary wire format using `DataView`-based bit manipulation.
All pack functions emit an OTel counter increment so throughput can be observed in production without adding latency to the hot path.
## Features
- `packFloat16` / `unpackFloat16` — 2-byte IEEE-754 half-precision encoding/decoding
- `packFloat32` / `unpackFloat32` — 4-byte IEEE-754 single-precision using `DataView.setFloat32`
- `packFloat64` / `unpackFloat64` — 8-byte IEEE-754 double-precision using `DataView.setFloat64`
- Correct handling of `NaN`, `+Infinity`, `-Infinity`, `+0`, and `-0`
- Subnormal Float16 support (values below `2^-14`)
- Range guards: `isInFloat16Range`, `isInFloat32Range`, `isInFloat64Range`
- Constants: `min16`, `max16`, `min32`, `max32`, `min64`, `max64`
- OTel counters for pack call throughput
## Usage: Basic Example
```ts
import {
packFloat16,
unpackFloat16,
packFloat32,
unpackFloat32,
packFloat64,
unpackFloat64,
} from "@suaveplan/utils/number";
// Float16 round-trip
const bytes16 = packFloat16(1.5); // Uint8Array(2)
const back16 = unpackFloat16(bytes16); // ~1.5
// Float32 round-trip
const bytes32 = packFloat32(3.14);
const back32 = unpackFloat32(bytes32); // ~3.14
// Float64 round-trip (lossless)
const bytes64 = packFloat64(Math.PI);
const back64 = unpackFloat64(bytes64); // Math.PI exactly
```
## Usage: Advanced Example
```ts
import {
packFloat16,
unpackFloat16,
isInFloat16Range,
isInFloat32Range,
max16,
max32,
} from "@suaveplan/utils/number";
// Check range before packing
function safePackFloat16(value: number): Uint8Array {
if (!isInFloat16Range(value)) {
throw new RangeError(`${value} is out of Float16 range`);
}
return packFloat16(value);
}
// Handle special values
console.log(unpackFloat16(packFloat16(Infinity))); // Infinity
console.log(unpackFloat16(packFloat16(-0))); // -0
console.log(Number.isNaN(unpackFloat16(packFloat16(NaN)))); // true
// max values
console.log(max16); // 65504
console.log(max32); // ~3.4028235e+38
// Range guards
console.log(isInFloat16Range(65504)); // true
console.log(isInFloat16Range(70000)); // false
console.log(isInFloat32Range(Number.MAX_VALUE)); // false
```
## API Reference
### `packFloat16(value: number): Uint8Array`
Encodes a number as a 2-byte Float16. Handles NaN, ±Infinity, subnormals. Increments OTel counter.
### `unpackFloat16(buffer: Uint8Array): number`
Decodes a 2-byte Uint8Array to a JavaScript number. Throws if buffer.length !== 2.
### `packFloat32(value: number): Uint8Array`
Encodes a number as a 4-byte Float32 using `DataView.setFloat32`. Increments OTel counter.
### `unpackFloat32(buffer: Uint8Array): number`
Decodes a 4-byte Uint8Array to a JavaScript number using `DataView.getFloat32`.
### `packFloat64(value: number): Uint8Array`
Encodes a number as an 8-byte Float64 using `DataView.setFloat64`. Lossless. Increments OTel counter.
### `unpackFloat64(buffer: Uint8Array): number`
Decodes an 8-byte Uint8Array to a JavaScript number using `DataView.getFloat64`.
### `isInFloat16Range(value: number): boolean`
Returns true when `value` is zero, ±Infinity, NaN, or within `[min16, max16]`.
### `isInFloat32Range(value: number): boolean`
Returns true when `value` is zero, ±Infinity, NaN, or within `[min32, max32]`.
### `isInFloat64Range(value: number): boolean`
Returns true when `value` is finite. Infinity and NaN return false.
### Constants
- `min16` — `2^-14` (~6.1e-5), minimum positive normal Float16
- `max16` — `65504`, maximum finite Float16
- `min32` — `2^-126`, minimum positive normal Float32
- `max32` — `(2 - 2^-23) * 2^127`, maximum finite Float32
- `min64` — `Number.MIN_VALUE`, minimum positive subnormal Float64
- `max64` — `Number.MAX_VALUE`, maximum finite Float64
## Implementation Notes
Float16 is not natively supported by `DataView`, so `packFloat16` and `unpackFloat16` implement the IEEE-754 half-precision format manually using bit shifting. The sign bit is bit 15, exponent is bits 14–10 (5 bits, biased by 15), and mantissa is bits 9–0 (10 bits). Subnormal numbers (exponent field = 0) are handled explicitly.
Float32 and Float64 use `DataView.setFloat32`/`setFloat64` which delegate to the CPU's native IEEE-754 implementation and are essentially free. The `unpack` functions construct a `DataView` using the buffer's `byteOffset` and `byteLength` to correctly handle sliced `Uint8Array` views.

View File

@@ -0,0 +1,145 @@
# object
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `object` module provides a comprehensive set of object manipulation utilities for working with plain objects in TypeScript. It covers deep cloning, recursive merging, key projection (pick/omit), nested path access, and structural flattening. All operations are non-mutating by default and use precise generics to preserve type information without requiring `any` casts.
The module integrates OpenTelemetry spans on the most expensive operations (`deepClone`, `deepMerge`) to enable tracing in production. Prototype pollution attempts (keys like `__proto__`, `constructor`, `prototype`) are caught and converted to typed `UtilsError` instances.
## Features
- `isObject` — checks for non-null, non-array objects
- `isPlainObject` — excludes Date, RegExp, Map, Set, etc.
- `isObjectOrArray` — `typeof value === "object" && value !== null`
- `isEmpty` — true when object has no own enumerable keys
- `deepClone` — recursive clone with Date/RegExp/Map/Set/Array support; circular ref detection
- `deepMerge` — recursive merge with `skipUndefined` option; prototype pollution guard
- `deepMergeAll` — variadic merge, later args win
- `pick` / `keepKeys` — project subset of keys
- `omit` / `removeKeys` — exclude subset of keys
- `getNestedValue` — dot-notation path read with optional default
- `setNestedValue` — dot-notation path write; creates intermediate objects
- `hasPath` — tests existence of a dot-notation path
- `flattenObject` — recursive flattening to dot-notation; configurable separator and prefix
## Usage: Basic Example
```ts
import { deepMerge, pick, omit, flattenObject } from "@suaveplan/utils/object";
const base = { a: 1, b: { x: 10, y: 20 } };
const patch = { b: { x: 99 } };
const merged = deepMerge(base, patch);
// => { a: 1, b: { x: 99, y: 20 } }
const subset = pick(base, ["a"]);
// => { a: 1 }
const flat = flattenObject({ a: { b: { c: 42 } } });
// => { "a.b.c": 42 }
```
## Usage: Advanced Example
```ts
import {
deepClone,
deepMergeAll,
getNestedValue,
setNestedValue,
hasPath,
} from "@suaveplan/utils/object";
// Deep clone with circular-reference detection
const original = { data: new Map([["key", new Date()]]) };
const clone = deepClone(original);
// clone.data is an independent Map with a new Date instance
// Variadic merge — later args win, undefined from source is skipped
const config = deepMergeAll(
{ timeout: 5000, retries: 3, debug: false },
{ retries: 5 },
{ debug: true }
);
// => { timeout: 5000, retries: 5, debug: true }
// Nested path operations
const obj: Record<string, unknown> = {};
setNestedValue(obj, "server.port", 8080);
console.log(getNestedValue(obj, "server.port")); // 8080
console.log(hasPath(obj, "server.port")); // true
console.log(hasPath(obj, "server.host")); // false
```
## API Reference
### `isObject(value: unknown): value is Record<string, unknown>`
Returns true for non-null, non-array objects.
### `isPlainObject(value: unknown): value is Record<string, unknown>`
Returns true only for objects whose prototype is `null` or `Object.prototype`.
### `isObjectOrArray(value: unknown): value is Record<string, unknown> | unknown[]`
Returns true for any non-null object (including arrays).
### `isEmpty(obj: Record<string, unknown>): boolean`
Returns true if the object has no own enumerable keys.
### `deepClone<T>(obj: T): T`
Recursively clones an object. Handles `Date`, `RegExp`, `Map`, `Set`, arrays, and plain objects. Throws `UtilsError(OBJECT.CIRCULAR)` on circular references. Instruments a span via OTel.
### `deepMerge<T>(target: T, source: Partial<T>, skipUndefined?: boolean): T`
Merges `source` into `target` recursively. Arrays are replaced (not merged). Throws `UtilsError(OBJECT.PROTOTYPE_POLLUTION)` on dangerous keys. Instruments a span via OTel.
### `deepMergeAll<T>(...objects: Partial<T>[]): T`
Variadic merge. Later arguments take precedence. Skips undefined source values.
### `pick<T, K>(obj: T, keys: K[]): Pick<T, K>`
Returns a new object with only the specified keys.
### `keepKeys<T, K>(obj: T, keys: K[]): Pick<T, K>`
Alias for `pick`.
### `omit<T, K>(obj: T, keys: K[]): Omit<T, K>`
Returns a new object without the specified keys.
### `removeKeys<T, K>(obj: T, keys: K[]): Omit<T, K>`
Alias for `omit`.
### `getNestedValue<T>(obj, path, defaultValue?): T | undefined`
Reads a value at a dot-notation path. Returns `defaultValue` (or `undefined`) when the path is missing.
### `setNestedValue(obj, path, value): void`
Writes a value at a dot-notation path. Creates intermediate plain objects as needed.
### `hasPath(obj, path): boolean`
Returns true if every segment of the dot-notation path exists and is accessible.
### `flattenObject(obj, prefix?, separator?): Record<string, unknown>`
Recursively flattens nested plain objects to a single level. Default separator is `"."`.
## Implementation Notes
`deepClone` passes a `WeakSet` through the recursion to track already-seen objects; this adds minimal overhead while correctly detecting cycles rather than infinite-looping. The OTel span wraps the entire public-facing call, not the internal recursion, to avoid noisy child spans.
`deepMerge` uses object spread for a shallow copy then selectively overwrites keys. The prototype pollution guard runs on every `in source` key, not just the changed ones. The `skipUndefined` flag is useful for config layering where you want later layers to express "leave this field as-is" by passing `undefined`.
All generic functions use `Record<string, unknown>` (not `any`) for the outer constraint. Key extraction via `for...in` + `Object.hasOwn` avoids inherited property iteration.

View File

@@ -0,0 +1,102 @@
# platform
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `platform` module provides an injectable abstraction layer for environment-specific system APIs such as memory usage, CPU usage, and platform detection. It enables universal packages to access runtime information without hard-coding assumptions about the execution environment. The default adapter reads from Node.js/Bun `process` APIs, while the injectable interface allows test code to supply any values without mocking.
The module detects Bun first (checking `typeof Bun !== "undefined"`), falls back to Node.js via `process.versions.node`, then browser (via `window`), and finally returns `"unknown"` for other environments such as Web Workers.
## Features
- `createPlatform(adapter?)` — factory that returns a platform instance with injectable adapter methods
- `getPlatform()` — returns `"bun" | "node" | "browser" | "unknown"` for the current environment
- `isNode()` — true when running in Node.js or Bun (both expose `process`)
- `isBrowser()` — true when `window` and `document` are defined
- `isBun()` — true when `typeof Bun !== "undefined"`
- Default adapter wraps `process.memoryUsage()` and `process.cpuUsage()` with OTel spans
- Throws `UtilsError(PLATFORM.UNAVAILABLE)` when the required process API is absent
- `MemoryUsage`, `CpuUsage`, `PlatformName` types and Zod schemas exported
## Usage: Basic Example
```ts
import { getPlatform, isNode, isBrowser, isBun } from "@suaveplan/utils/platform";
const env = getPlatform();
// => "node" | "bun" | "browser" | "unknown"
if (isNode()) {
console.log("Running under Node.js or Bun");
}
if (isBrowser()) {
console.log("Running in a browser");
}
```
## Usage: Advanced Example
```ts
import { createPlatform } from "@suaveplan/utils/platform";
// Inject a custom adapter for testing or custom environments
const platform = createPlatform({
getMemoryUsage: () => ({ heapUsed: 1024 * 1024, heapTotal: 4 * 1024 * 1024 }),
getCpuUsage: () => ({ user: 0, system: 0 }),
getPlatform: () => "node",
});
const mem = platform.getMemoryUsage();
console.log(`Heap used: ${mem.heapUsed} bytes`);
const cpu = platform.getCpuUsage();
console.log(`User CPU: ${cpu.user} µs`);
```
## API Reference
### `createPlatform(adapter?: Partial<PlatformAdapter>): PlatformAdapter`
Creates a platform instance. Any method not provided in `adapter` falls back to the default Node/Bun implementation.
### `getPlatform(): PlatformName`
Returns the current platform name using the module-level default adapter.
### `isNode(): boolean`
Returns true when `getPlatform()` returns `"node"` or `"bun"`.
### `isBrowser(): boolean`
Returns true when `getPlatform()` returns `"browser"`.
### `isBun(): boolean`
Returns true when `typeof Bun !== "undefined"`. More precise than `getPlatform() === "bun"`.
### `PlatformAdapter`
Interface with `getMemoryUsage(): MemoryUsage`, `getCpuUsage(): CpuUsage`, and `getPlatform(): PlatformName`.
### `MemoryUsage`
`{ heapUsed, heapTotal, external?, arrayBuffers?, rss? }` — all in bytes.
### `CpuUsage`
`{ user, system }` — CPU time in microseconds.
### `PlatformName`
`"browser" | "node" | "bun" | "unknown"`.
## Implementation Notes
The injectable adapter pattern avoids `vi.mock()` in tests — pass a custom adapter to `createPlatform()` instead. The default adapter is built lazily in `_buildDefaultAdapter()` and bound at `createPlatform()` call time so partial overrides compose correctly.
OTel spans wrap `getMemoryUsage()` and `getCpuUsage()` in the default adapter to trace latency of these syscalls in production. The spans use the TRACER_NAME `"@suaveplan/utils/platform"`.
The `isNode()` function returns true for both `"node"` and `"bun"` because Bun exposes the full `process` API and is generally compatible with Node.js code. Use `isBun()` when you specifically need to distinguish the two.

View File

@@ -0,0 +1,94 @@
# pseudo
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `pseudo` module provides `pseudoRandom`, a deterministic array element selector that uses a seed-driven shuffle to pick reproducible results. Unlike `Math.random()`, which is non-deterministic, `pseudoRandom` guarantees that the same array, index, and seed always produce the same output — enabling reproducible sampling in tests, content variation systems, and A/B assignments.
The function shuffles a copy of the input array using a Fisher-Yates-like algorithm driven by a hash of the seed, then returns the element at the specified index. The original array is never mutated.
## Features
- Deterministic output for the same `(arr, index, seed)` triple
- Injectable `rng` option for pure-function testing without mocking
- Throws `UtilsError(RANDOM.EMPTY_ARRAY)` for empty input arrays
- Throws `UtilsError(RANDOM.INDEX_OUT_OF_BOUNDS)` for invalid indices
- Fully generic: `pseudoRandom<T>(arr: T[], ...): T`
- OTel counter on every call for throughput observability
- Zero mutation of the source array
## Usage: Basic Example
```ts
import { pseudoRandom } from "@suaveplan/utils/random";
const themes = ["ocean", "forest", "desert", "arctic"];
// Deterministic — always returns the same theme for seed 0.42
const theme = pseudoRandom(themes, 0, 0.42);
console.log(theme); // e.g., "forest"
// Without explicit seed — uses Math.random() internally
const random = pseudoRandom(themes, 0);
```
## Usage: Advanced Example
```ts
import { pseudoRandom } from "@suaveplan/utils/random";
// Reproducible A/B variant selection based on user ID hash
function getUserVariant(userId: string, variants: string[]): string {
// Convert user ID to a seed in [0, 1)
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = (hash * 31 + userId.charCodeAt(i)) % 1_000_000;
}
const seed = hash / 1_000_000;
return pseudoRandom(variants, 0, seed);
}
const variant = getUserVariant("user-1234", ["control", "treatment-a", "treatment-b"]);
// Always the same for "user-1234"
// Injectable RNG for testing without mocking Math.random
const result = pseudoRandom(["a", "b", "c"], 1, undefined, { rng: () => 0.7 });
```
## API Reference
### `pseudoRandom<T>(arr: T[], index: number, seed?: number, options?: PseudoRandomOptions): T`
Selects an element from `arr` using a seeded shuffle.
**Parameters:**
- `arr` — `T[]` — Non-empty source array. Not mutated.
- `index` — `number` — Position in the shuffled result to return. Must be in `[0, arr.length)`.
- `seed` — `number | undefined` — Value in `[0, 1)` driving the shuffle. Defaults to `options.rng()` or `Math.random()` if omitted.
- `options.rng` — `() => number` — Optional injectable RNG that replaces `Math.random()` when `seed` is not provided.
**Returns:** `T` — The element at position `index` in the shuffled copy.
**Throws:**
- `UtilsError(RANDOM.EMPTY_ARRAY)` — when `arr.length === 0`.
- `UtilsError(RANDOM.INDEX_OUT_OF_BOUNDS)` — when `index < 0` or `index >= arr.length`.
### `PseudoRandomOptions`
```ts
interface PseudoRandomOptions {
rng?: () => number;
}
```
## Implementation Notes
The shuffle uses a simple hash function `h(x) = (sin(x * 12.9898) * 43758.5453) % 1` applied iteratively. This is not cryptographically secure and should not be used for security-sensitive randomness — use `@suaveplan/crypto` for that purpose. It is deterministic within floating-point precision, making it suitable for reproducible UI variation and test fixtures.
The `Math.abs` call in `j = Math.floor(Math.abs(hash) * (i + 1))` ensures the hash value (which can be negative) maps to a valid index range regardless of the sign produced by the sine function.
The `options.rng` parameter is separate from `seed` intentionally: `seed` is a numeric influence on the shuffle; `rng` is a replacement for `Math.random()` used only when `seed` is not provided. This distinction allows tests to both inject a deterministic seed directly and inject a deterministic RNG.

View File

@@ -0,0 +1,107 @@
# sound
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `sound` module provides lightweight utilities for managing audio sprite variations in Howler.js-based (or compatible) audio engines. Real-world games and interactive UIs typically pack many short audio clips into a single sprite file to minimize HTTP requests. Each clip appears as a named key like `footstep_1`, `footstep_2`, and `footstep_3`. This module extracts those variation groups and provides a random-selection helper, enabling natural-sounding audio without repetition fatigue — commonly known as the "machine-gun effect" problem in audio design.
## Features
- **`parseSoundVariations`**: scans a Howler.js sprite map and groups keys by base name, producing a `SoundVariations` map of `{baseName: [1, 2, 3, ...]}`.
- **`getSoundKey`**: randomly selects one variation number from the pool and returns the full sprite key (e.g. `"footstep_2"`). Throws a typed `UtilsError` with code `SOUND.NO_VARIATIONS` when the requested name is not in the map.
- **`convertSprites`**: semantic alias for `parseSoundVariations`, matching the "convert" terminology used by audio-engine integrations.
- **`SoundSprites` type**: `Record<string, [number, number]>` describing Howler.js sprite timing data (offset, duration in milliseconds).
- **`SoundVariations` type**: `Record<string, number[]>` describing base-name to variation-number pool.
- **Zod schemas**: `soundSpriteEntrySchema`, `soundSpritesSchema`, `soundVariationsSchema` for schema-driven validation pipelines.
## Usage: Basic Example
```typescript
import { parseSoundVariations, getSoundKey } from "@suaveplan/utils/sound";
const sprites = {
footstep_1: [0, 100],
footstep_2: [100, 100],
footstep_3: [200, 100],
jump_1: [300, 150],
};
const variations = parseSoundVariations(sprites);
// { footstep: [1, 2, 3], jump: [1] }
const key = getSoundKey("footstep", variations);
// "footstep_1", "footstep_2", or "footstep_3" — randomly selected
```
## Usage: Advanced Example
```typescript
import {
convertSprites,
getSoundKey,
type SoundSprites,
} from "@suaveplan/utils/sound";
import Howl from "howler";
const rawSprites: SoundSprites = {
walk_1: [0, 400],
walk_2: [400, 400],
walk_3: [800, 400],
run_1: [1200, 200],
run_2: [1400, 200],
ambientLoop: [1600, 5000], // no underscore+number → ignored by convertSprites
};
const howl = new Howl({ src: ["audio.webm"], sprite: rawSprites });
const variations = convertSprites(rawSprites);
// { walk: [1,2,3], run: [1,2] } — ambientLoop is excluded
function playWalk(): void {
const key = getSoundKey("walk", variations);
howl.play(key);
}
```
## API Reference
### `parseSoundVariations(sprites: SoundSprites): SoundVariations`
Parse a sprite map and extract variation groups.
| Parameter | Type | Description |
|-----------|---------------|--------------------------------------|
| `sprites` | `SoundSprites` | Howler.js sprite map. |
**Returns:** `SoundVariations` — base names mapped to arrays of variation numbers. Keys that do not follow `<base>_<integer>` are silently ignored.
### `getSoundKey(name: string, variations: SoundVariations): string`
Randomly select a sprite key for a named sound.
| Parameter | Type | Description |
|--------------|------------------|------------------------------------------|
| `name` | `string` | Base sound name. |
| `variations` | `SoundVariations`| Variation map from `parseSoundVariations`.|
**Returns:** `string` — full sprite key e.g. `"footstep_2"`.
**Throws:** `UtilsError` (`SOUND.NO_VARIATIONS`) if the name is not in the map.
### `convertSprites(sprites: SoundSprites): SoundVariations`
Semantic alias for `parseSoundVariations`. Identical behavior.
### Types
- `SoundSprites = Record<string, [number, number]>` — Howler.js sprite timing.
- `SoundVariations = Record<string, number[]>` — base name → variation numbers.
## Implementation Notes
Key parsing uses `lastIndexOf("_")` rather than `split("_")` to correctly handle base names that themselves contain underscores (e.g. `ambient_rain_1` → base `ambient_rain`, variation `1`). The suffix is validated with `Number.isInteger(Number(suffix))` to reject non-numeric suffixes such as `"a"` or empty strings.
Random selection uses `Math.random()` which is sufficient for audio variation purposes. If deterministic selection is required (e.g. in tests), callers should mock `Math.random` or extract the selection logic with a custom RNG parameter in a future enhancement.
No OTel instrumentation is added here because these are pure, synchronous utility functions without side effects or external I/O. Telemetry can be added at the call site by callers who already have a span context.

View File

@@ -0,0 +1,99 @@
# string
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `string` module provides two focused, dependency-free string utilities: `generateId` for creating random, URL-safe identifier strings using `crypto.randomUUID()`, and `parseTextWithLinks` for extracting HTTP/HTTPS URLs from arbitrary plain text. Both functions are pure and work without modification in any JavaScript runtime — browser, Node.js, Bun, and edge workers — making them safe to include in universal packages with zero additional dependencies.
## Features
- **`generateId`**: generates a 32-character random hex identifier by default (UUID with dashes stripped). Accepts an optional `length` parameter to produce identifiers of any length by trimming or concatenating UUID material.
- **`parseTextWithLinks`**: scans plain text for `http://` and `https://` URLs using a conservative regex, returning an array of `TextLink` objects with `url`, `text`, and `index` fields.
- **TypeScript types**: `GenerateIdOptions` and `TextLink` are exported for use in typed call sites.
- **Zod schemas**: `idSchema`, `generateIdOptionsSchema`, and `textLinkSchema` for schema-driven validation pipelines.
- **Cross-runtime crypto**: uses `globalThis.crypto` in browser/Bun and falls back to `node:crypto` `webcrypto` in Node.js test environments.
## Usage: Basic Example
```typescript
import { generateId, parseTextWithLinks } from "@suaveplan/utils/string";
// Generate a random 32-char identifier
const id = generateId();
// e.g. "a3f7c12d9b4e8a1f0c2d5e6f7a8b9c0d"
// Generate an 8-char identifier for short codes
const shortCode = generateId({ length: 8 });
// e.g. "a3f7c12d"
// Extract URLs from text
const links = parseTextWithLinks("Visit https://example.com and https://docs.example.com/guide");
// [
// { url: "https://example.com", text: "https://example.com", index: 6 },
// { url: "https://docs.example.com/guide", text: "https://docs.example.com/guide", index: 28 }
// ]
```
## Usage: Advanced Example
```typescript
import { generateId, parseTextWithLinks } from "@suaveplan/utils/string";
// Build a short token for email verification
function createVerificationToken(): string {
return generateId({ length: 24 });
}
// Linkify a user's comment for display
function extractLinksFromComment(comment: string) {
const links = parseTextWithLinks(comment);
return links.map(link => ({
href: link.url,
label: link.text,
position: link.index,
}));
}
const comment = "Check out https://github.com/SuavePlan/suaveplan for the source!";
const extracted = extractLinksFromComment(comment);
// [{ href: "https://github.com/SuavePlan/suaveplan", label: "...", position: 10 }]
```
## API Reference
### `generateId(opts?: GenerateIdOptions): string`
Generate a random URL-safe identifier string.
| Parameter | Type | Description |
|-----------|---------------------|--------------------------------------------|
| `opts` | `GenerateIdOptions` | Optional. `{ length?: number }` — desired length. Defaults to 32. |
**Returns:** `string` — random hex string of the requested length.
**Does not throw.** Crypto is always available in supported runtimes.
### `parseTextWithLinks(text: string): TextLink[]`
Extract HTTP/HTTPS URLs from a plain-text string.
| Parameter | Type | Description |
|-----------|----------|---------------------------|
| `text` | `string` | Text to scan for URLs. |
**Returns:** `TextLink[]` — array of extracted links in order of appearance. Each entry has `url`, `text`, and `index` fields.
### Types
- `GenerateIdOptions` — `{ length?: number }`.
- `TextLink` — `{ url: string; text: string; index: number }`.
## Implementation Notes
`generateId` uses `crypto.randomUUID()` rather than `Math.random().toString(36)` because UUID generation uses a cryptographically secure PRNG, giving 122 bits of entropy per call — sufficient for database primary keys and session tokens. For lengths beyond 32 characters, additional UUIDs are generated and concatenated; this is O(⌈length/32⌉) UUID calls.
`parseTextWithLinks` creates a fresh `RegExp` instance on each call (from the source of a module-level `const` regex literal) to avoid stateful `lastIndex` bugs when the same regex is reused across calls. The URL regex conservatively stops at whitespace and a small set of shell-special characters; it is not a full RFC 3986 parser, so some edge-case URLs (e.g. those containing unescaped brackets) may be truncated.
Both functions are side-effect-free and safe to call in concurrent contexts.

View File

@@ -0,0 +1,92 @@
# types/types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `types` module declares shared TypeScript utility types used across the `@suaveplan/utils` package. These are compile-time-only definitions — there is no runtime JavaScript output from this file. By centralizing common type aliases here, individual feature modules avoid duplicating definitions and consumers get consistent, named types that appear clearly in IDE tooltips and type error messages.
## Features
- **`PlainObject`**: alias for `Record<string, unknown>` — a plain JavaScript object with string keys and arbitrary values. Used as the parameter type for object-manipulation utilities like `pick`, `omit`, `deepMerge`, and `flattenObject`.
- **`DeepPartial<T>`**: recursive utility type that makes all properties of `T` and every nested object optional. Useful for partial-update payloads and test fixture builders.
- **`Primitive`**: union of all JavaScript primitive value types (`string | number | boolean | bigint | symbol | null | undefined`). Used to guard leaf-node values in recursive object traversal.
## Usage: Basic Example
```typescript
import type { PlainObject, Primitive } from "@suaveplan/utils";
function isLeaf(value: unknown): value is Primitive {
return (
value === null ||
value === undefined ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
typeof value === "bigint" ||
typeof value === "symbol"
);
}
function countKeys(obj: PlainObject): number {
return Object.keys(obj).length;
}
```
## Usage: Advanced Example
```typescript
import type { DeepPartial, PlainObject } from "@suaveplan/utils";
interface Config {
server: { host: string; port: number };
database: { url: string; pool: number };
}
// Accept partial overrides for testing
function createConfig(overrides: DeepPartial<Config> = {}): Config {
return {
server: { host: "localhost", port: 3000, ...overrides.server },
database: { url: "postgres://localhost/dev", pool: 5, ...overrides.database },
};
}
// Store arbitrary metadata
const meta: PlainObject = { version: "1.0", tags: ["prod"] };
```
## API Reference
### `PlainObject`
```ts
type PlainObject = Record<string, unknown>
```
A plain JS object with string keys and `unknown` values. Equivalent to `{ [key: string]: unknown }`.
### `DeepPartial<T>`
```ts
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T
```
Recursively makes all properties optional. For non-object types (primitives, arrays) the type is returned unchanged.
### `Primitive`
```ts
type Primitive = string | number | boolean | bigint | symbol | null | undefined
```
The complete set of JavaScript primitive value types.
## Implementation Notes
These types ship zero runtime bytes — they exist only in the type-checking phase and are erased during compilation. The `types` directory is explicitly excluded from Istanbul coverage reporting in `vitest.config.ts` (`"src/types/**"`).
`DeepPartial` uses a conditional mapped type rather than the simpler `Partial<T>` to recurse into nested objects. It does not recurse into arrays intentionally — array elements are left as-is, which is the expected behavior for most use cases.

View File

@@ -0,0 +1,86 @@
# validate
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `validate` module provides lightweight, format-only type-guard validators for common binary encoding formats. Unlike full parsers that decode and reconstruct data, these guards perform only surface-level structural checks — enough to narrow the TypeScript type and reject obviously malformed inputs before passing data to more expensive operations such as cryptographic decoding, database writes, or network transmission. They are intentionally thin so callers pay no allocation cost beyond the guard itself.
## Features
- **`isHex`**: validates that a value is a non-empty, even-length string consisting exclusively of hexadecimal characters (`[A-Fa-f0-9]`). Returns `true` only when the string encodes at least one complete byte (two hex digits). Acts as a TypeScript type guard narrowing `unknown` to `string`.
- **Zero dependencies**: no external imports — just a regex and a `typeof` check.
- **TypeScript narrowing**: each guard is declared as a type-predicate (`i is string`) so TypeScript narrows the type in the `if`-branch automatically.
## Usage: Basic Example
```typescript
import { isHex } from "@suaveplan/utils/validate";
function decodePayload(raw: unknown): Uint8Array {
if (!isHex(raw)) {
throw new TypeError(`Expected hex string, got ${typeof raw}`);
}
// TypeScript now knows raw is string
const bytes = new Uint8Array(raw.length / 2);
for (let i = 0; i < raw.length; i += 2) {
bytes[i / 2] = parseInt(raw.slice(i, i + 2), 16);
}
return bytes;
}
```
## Usage: Advanced Example
```typescript
import { isHex } from "@suaveplan/utils/validate";
// Validate incoming webhook signature before crypto ops
function verifySignature(payload: string, sigHeader: unknown): boolean {
if (!isHex(sigHeader)) {
console.warn("Invalid signature format — expected hex");
return false;
}
// sigHeader is now narrowed to string
const sigBytes = Buffer.from(sigHeader, "hex");
// ... HMAC verification ...
return true;
}
// Use in Zod custom refinement
import z from "zod";
const hexStringSchema = z.string().refine(isHex, {
message: "Must be a non-empty even-length hexadecimal string",
});
```
## API Reference
### `isHex(i: unknown): i is string`
Validate that a value is a well-formed hexadecimal byte string.
| Parameter | Type | Description |
|-----------|-----------|--------------------------|
| `i` | `unknown` | Value to test. |
**Returns:** `boolean` — `true` if and only if `i` is a `string` with length > 0, even length, and only `[A-Fa-f0-9]` characters.
**Does not throw.**
**Rules:**
- Empty string → `false`.
- Odd-length string → `false` (not a complete sequence of bytes).
- Any non-hex character (`g`–`z`, `G`–`Z`, digits > 9, symbols) → `false`.
- Non-string input → `false`.
## Implementation Notes
The guard uses the regex `/^([A-Fa-f0-9]{2})+$/` which enforces both character validity and even length in a single pass. The `+` quantifier (one or more pairs) additionally rejects empty strings without a separate length check.
The function is declared as a `const` arrow (rather than a `function` declaration) to emphasize that it is a pure predicate with no side effects and no prototype overhead.
The regex is compiled once at module load time because it is a literal constant in the regex source, eliminating per-call compilation cost.

View File

@@ -0,0 +1,105 @@
# validation/size
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `validation/size` module provides consistent byte-size validation, formatting, parsing, and truncation utilities for use across transport and networking packages. Oversized payloads are a common Denial of Service (DoS) vector — a single large message can exhaust memory, starve the event loop, or cause upstream timeouts. By centralizing size enforcement in one place, every consumer gets uniform behavior, shared OTel counters (so violations appear in observability dashboards without per-package instrumentation), and no duplicated regex or threshold logic.
## Features
- **`SIZE_LIMITS`**: named constants for five common size tiers — SMALL (10 KB), MEDIUM (100 KB), LARGE (1 MB), XLARGE (10 MB), MAX (100 MB).
- **`DEFAULT_MAX_MESSAGE_SIZE`**: 1 MB, the default upper bound used by `validateSize` and `isWithinSizeLimit`.
- **`getDataSize`**: returns the byte length of a string (UTF-8 via `TextEncoder`), `Uint8Array`, `ArrayBuffer`, or `number` (always 8 bytes as IEEE 754 float64).
- **`validateSize`**: throws a typed `UtilsError` with code `VALIDATION.SIZE_EXCEEDED` when data exceeds the limit. Emits OTel counters for total calls and violation count.
- **`isWithinSizeLimit`**: non-throwing boolean version of `validateSize`.
- **`formatSize`**: converts a byte count to a human-readable string such as `"1.50 MB"`.
- **`parseSize`**: converts size strings (`"1KB"`, `"1.5MB"`, `"100"`) to byte integers. Throws `UtilsError` with code `VALIDATION.PARSE_FAILED` for unrecognised formats.
- **`truncateToSize`**: truncates a string to fit within a byte limit using binary search for correctness with multibyte UTF-8 content.
- **Zod schemas**: `sizeBytesSchema`, `sizeStringSchema`, `sizeLimitsSchema`.
## Usage: Basic Example
```typescript
import { validateSize, SIZE_LIMITS } from "@suaveplan/utils/validation";
// Validate an API request body
function handleRequest(body: string): void {
validateSize(body, SIZE_LIMITS.MEDIUM); // throws if > 100 KB
processBody(body);
}
```
## Usage: Advanced Example
```typescript
import {
formatSize,
getDataSize,
isWithinSizeLimit,
parseSize,
truncateToSize,
DEFAULT_MAX_MESSAGE_SIZE,
} from "@suaveplan/utils/validation";
// Report payload size before sending
function sendWebhook(payload: string): void {
const size = getDataSize(payload);
console.log(`Sending ${formatSize(size)} payload`);
if (!isWithinSizeLimit(payload, DEFAULT_MAX_MESSAGE_SIZE)) {
// Gracefully truncate rather than fail
const truncated = truncateToSize(payload, DEFAULT_MAX_MESSAGE_SIZE);
console.warn(`Payload truncated from ${formatSize(size)} to ${formatSize(getDataSize(truncated))}`);
deliver(truncated);
return;
}
deliver(payload);
}
// Parse a config-defined size limit
const configuredLimit = parseSize(process.env.MAX_PAYLOAD_SIZE ?? "1MB");
// → 1048576
```
## API Reference
### `getDataSize(data: string | Uint8Array | ArrayBuffer | number): number`
Return the byte size of the given data.
| Type | Measurement |
|--------------|---------------------------|
| `string` | UTF-8 bytes via TextEncoder |
| `Uint8Array` | `byteLength` |
| `ArrayBuffer`| `byteLength` |
| `number` | Always 8 (float64) |
### `validateSize(data, maxSize?): void`
Throw `UtilsError` (`VALIDATION.SIZE_EXCEEDED`) if data exceeds `maxSize` (default 1 MB). Emits OTel counters.
### `isWithinSizeLimit(data, maxSize?): boolean`
Non-throwing boolean check. Returns `true` when data fits within `maxSize`.
### `formatSize(bytes: number, decimals?: number): string`
Format bytes as `"1.00 KB"`, `"1.50 MB"`, etc. Defaults to 2 decimal places.
### `parseSize(sizeStr: string): number`
Parse `"100"`, `"1KB"`, `"1.5MB"` to byte integers. Throws `UtilsError` (`VALIDATION.PARSE_FAILED`) for invalid input.
### `truncateToSize(data: string, maxSize: number): string`
Binary-search truncation for UTF-8 safety. Returns input unchanged if already within limit.
## Implementation Notes
`getDataSize` for strings uses `TextEncoder.encode().byteLength` rather than `Blob` or `Buffer.byteLength` because `TextEncoder` is available in all modern runtimes (browser, Node.js >= 11, Bun) and is synchronous with no I/O overhead.
`truncateToSize` uses binary search (O(log n) character positions) rather than linear slicing to handle the case where multibyte characters sit exactly at the boundary. The algorithm always produces the longest valid substring within the byte limit.
OTel counters are created once at module load using the `@suaveplan/utils/validation` meter. If no OTel SDK is configured, the NoopMeter silently ignores all calls.