Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .well-known/agent-skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,14 @@
"description": "Use when transforming plain objects functionally — Record utilities for map/filter/pick/omit/reduce, plus partition and filterMap for advanced object manipulation.",
"files": ["SKILL.md"]
},
{
"name": "tsentials-string",
"description": "Use when converting string casing — toPascalCase/toCamelCase/toKebabCase/toSnakeCase/toMacroCase/toTrainCase/toTitleCase/toUnderscoreCamelCase, all word-boundary aware (camelCase, PascalCase, consecutive-uppercase runs like \"XMLParser\", whitespace, hyphens, underscores).",
"files": ["SKILL.md"]
},
{
"name": "tsentials-meta",
"description": "Use when deciding which tsentials module to use — overview of all 18 subpath imports organized by concern, install command, and a quick-reference table mapping problems to modules.",
"description": "Use when deciding which tsentials module to use — overview of all 19 subpath imports organized by concern, install command, and a quick-reference table mapping problems to modules.",
"files": ["SKILL.md"]
}
]
Expand Down
21 changes: 14 additions & 7 deletions .well-known/agent-skills/tsentials-clone/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Hybrid implementation: tries native `structuredClone()` first; if unavailable (R
| Type | Behavior |
|------|---------|
| Primitives (string, number, boolean, null, undefined, bigint) | Returned as-is (immutable) |
| Plain object | Deep copy — `__proto__` preserved |
| Plain object | Deep copy of own enumerable keys — result is always a plain `Object.prototype` object; class instances lose their custom prototype/methods (see note below) |
| Array (including sparse) | Deep copy — holes preserved |
| Date | `new Date(timestamp)` |
| RegExp | `new RegExp(source, flags)` |
Expand All @@ -68,10 +68,14 @@ Hybrid implementation: tries native `structuredClone()` first; if unavailable (R
| ArrayBuffer | `buffer.slice(0)` |
| DataView | Buffer + byteOffset + byteLength |
| TypedArray (Uint8Array, Float64Array, BigInt64Array, …) | Buffer cloned separately |
| Error and subclasses | message, name, cause, custom props (code, statusCode, …) |
| Error (built-in: `Error`, `TypeError`, `RangeError`, …) | message, name, cause preserved; subclass identity kept |
| Boolean / Number / String wrapper objects | `Object(valueOf())` |
| Circular reference | Tracked via WeakMap — preserved correctly |
| SharedArrayBuffer | Same reference returned (shared memory semantics) |
| SharedArrayBuffer | **Native `structuredClone` (default in Node ≥ 17 / modern browsers):** returns a *new* `SharedArrayBuffer` instance backed by the same shared memory — not the same reference. **Recursive fallback only** (no native `structuredClone`): returns the exact same reference |

> **Important caveat — custom Error subclasses and custom properties:** `deepClone` tries native `structuredClone` first, and it does **not** throw for `Error` objects — so the recursive fallback (which is the part of this library that copies custom properties and preserves user-defined subclasses) is *not* reached for a standalone `Error` in Node.js or a browser. Under native `structuredClone`, a **user-defined** `Error` subclass becomes a plain `Error` (its class identity and any custom `name` are lost), and **any custom enumerable properties** (`code`, `statusCode`, etc.) are silently dropped. Only in environments without native `structuredClone` (e.g. some React Native/Hermes builds) does the recursive fallback run and actually preserve subclass identity and custom properties. Verified empirically — see the corrected example below.
>
> **Custom prototypes:** for the same reason, cloning a class instance (not a plain object literal) with `deepClone` produces a plain object — it does **not** remain `instanceof YourClass` and loses its methods. Use `Cloneable<T>` (see below) for class instances that must retain their type/methods.

### Graceful degradation

Expand Down Expand Up @@ -107,11 +111,13 @@ deepClone({ createdAt: new Date(), tags: new Set(['a', 'b']) });
const buf = new Uint8Array([1, 2, 3]);
const bufClone = deepClone(buf);

// Error with custom properties
// Error — under native structuredClone (default in Node/browsers), only
// message/name/cause survive; custom own properties are dropped
const err = Object.assign(new Error('fail'), { code: 'ERR_X', statusCode: 500 });
const errClone = deepClone(err);
errClone.code; // 'ERR_X'
errClone.statusCode; // 500
errClone.message; // 'fail'
errClone instanceof Error; // true
errClone.code; // undefined — custom properties are NOT preserved here

// Graceful degradation — never throws
deepClone({ fn: () => 42 }); // { fn: () => 42 } — same reference
Expand Down Expand Up @@ -184,7 +190,8 @@ const changed = working.filter((p, i) => p.price !== entities[i]?.price);
- Circular reference support works in both native and fallback modes
- `Function` values are returned by reference (closures cannot be copied)
- `WeakMap` / `WeakSet` cannot be cloned — use `Map` / `Set` if you need copyable contents
- Error subclass custom properties (`code`, `statusCode`, etc.) are cloned automatically
- Error custom properties (`code`, `statusCode`, etc.) and custom Error subclasses are **not** preserved under native `structuredClone` (the default in Node.js/browsers) — they only survive via the recursive fallback (no native `structuredClone` available). Do not rely on custom Error properties surviving `deepClone()` in Node
- Class instances lose their custom prototype/methods through `deepClone()` — they come back as plain objects; use `Cloneable<T>` instead if type/methods must be retained
- `cloneArray()` calls `.clone()` on each item — items must implement `Cloneable<T>`
- Always deep-copy nested collections inside `clone()` — a shallow copy defeats the purpose
- For simple value objects or DTOs, prefer `deepClone()` over implementing `Cloneable<T>`
6 changes: 3 additions & 3 deletions .well-known/agent-skills/tsentials-errors/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ npm install tsentials
## Import

```typescript
import { Err, ErrorMetadata } from 'tsentials/errors';
import type { AppError, ErrorType } from 'tsentials/errors';
import { Err, ErrorMetadata, ErrorType } from 'tsentials/errors';
import type { AppError } from 'tsentials/errors';
```

> **Important:** Import `Err` from `'tsentials/errors'` — NOT from `'tsentials/result'`.
Expand Down Expand Up @@ -92,7 +92,7 @@ error.metadata // ErrorMetadata | undefined (ReadonlyMap<string, unknown>)
## ErrorType Enum

```typescript
import type { ErrorType } from 'tsentials/errors';
import { ErrorType } from 'tsentials/errors';

ErrorType.Failure // general failure
ErrorType.Validation // 400-class, user input errors
Expand Down
4 changes: 3 additions & 1 deletion .well-known/agent-skills/tsentials-http/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,15 @@ const deleted = await RequestBuilder.delete(`https://api.example.com/users/${id}
.header('Authorization', `Bearer ${token}`)
.send<void>();

// Raw body (non-JSON)
// Raw (non-JSON) body on POST/PUT/PATCH — .body() bypasses JSON serialization entirely
const uploaded = await RequestBuilder.post('https://api.example.com/upload')
.header('Content-Type', 'text/plain')
.body(rawText)
.send<UploadResult>();
```

`.json(value)` stringifies `value` and sets `Content-Type: application/json`. `.body(raw)` sends `raw` (a `BodyInit` — string, `Blob`, `FormData`, `ArrayBuffer`, ...) untouched, with no assumption that it's JSON — `send<T>()` never runs `JSON.parse`/`JSON.stringify` on a `.body()` payload, so it can't throw on non-JSON content. Calling `.json()` after `.body()` (or vice versa) simply replaces whichever was set last.

### RequestBuilder API

| Static Factory | Returns |
Expand Down
2 changes: 1 addition & 1 deletion .well-known/agent-skills/tsentials-json/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ isJson({ key: undefined }); // false — undefined not valid JSON
`safeJsonParse` returns `Result<Json>` so it chains directly into any railway pipeline:

```typescript
import { Result } from 'tsentials/result';
import { Result, fromAsync } from 'tsentials/result';
import { safeJsonParse, isJsonObject, parseAndValidate } from 'tsentials/json';

// Chain into Result pipeline
Expand Down
5 changes: 4 additions & 1 deletion .well-known/agent-skills/tsentials-meta/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: tsentials-meta
description: Use when deciding which tsentials module to use — overview of all 18 subpath imports organized by concern, install command, and a quick-reference table mapping problems to modules.
description: Use when deciding which tsentials module to use — overview of all 19 subpath imports organized by concern, install command, and a quick-reference table mapping problems to modules.
---

# tsentials — Module Index
Expand All @@ -26,6 +26,7 @@ npm install tsentials
| `tsentials/maybe` | `Maybe<T>`, `tryFirst`, `tryFind`, `choose` | `tsentials-maybe` |
| `tsentials/union` | `Union<T>` discriminated union utility | `tsentials-union` |
| `tsentials/function` | `pipe`, `flow`, `identity`, `constant`, `flip` | `tsentials-function` |
| `tsentials/string` | `toPascalCase`, `toCamelCase`, `toKebabCase`, `toSnakeCase`, `toMacroCase`, `toTrainCase`, `toTitleCase`, `toUnderscoreCamelCase` | `tsentials-string` |

### Data Structures & Types

Expand Down Expand Up @@ -96,6 +97,7 @@ npm install tsentials
| Composable equality checks | `tsentials/eq` |
| Type-safe sorting and ordering | `tsentials/ord` |
| Composable boolean predicates | `tsentials/predicate` |
| Convert between naming conventions (camelCase, kebab-case, snake_case, ...) | `tsentials/string` |

---

Expand Down Expand Up @@ -128,6 +130,7 @@ import type { Cloneable } from 'tsentials/clone';
import { safeJsonParse, safeJsonStringify, parseAndValidate } from 'tsentials/json';
import { isJson, isJsonObject } from 'tsentials/json';
import type { Json, JsonObject } from 'tsentials/json';
import { toPascalCase, toCamelCase, toKebabCase, toSnakeCase, toMacroCase, toTrainCase, toTitleCase, toUnderscoreCamelCase } from 'tsentials/string';
```

---
Expand Down
3 changes: 2 additions & 1 deletion .well-known/agent-skills/tsentials-record/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Record.filter(users, u => u.name !== 'Bob');
Record.filterMap(users, u => u.name.length > 3 ? u.name : null);

// Modify
Record.upsert(users, 'c', { name: 'Charlie' });
Record.upsert(users, 'b', { name: 'Bobby' }); // { a: {...}, b: { name: 'Bobby' } }
Record.remove(users, 'b');
Record.pick(users, 'a');
Record.omit(users, 'b');
Expand All @@ -38,3 +38,4 @@ Record.partition(users, u => u.name.startsWith('A'));

- Use `Record.map` instead of `Object.entries(...).reduce(...)`.
- Use `pick` / `omit` for whitelist/blacklist field selection.
- `upsert`'s `key` must belong to the record's existing key union — `Record.upsert(users, 'c', ...)` above would not compile since `users` only has keys `'a' | 'b'`. Widen the record's type first if you need to add a genuinely new key.
1 change: 1 addition & 0 deletions .well-known/agent-skills/tsentials-result/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ npm install tsentials

```typescript
import { Result, ResultChain, chain, ResultAsync, fromAsync } from 'tsentials/result';
import type { VoidResult } from 'tsentials/result';
import { maybeToResult, resultToMaybe } from 'tsentials/result';
```

Expand Down
4 changes: 3 additions & 1 deletion .well-known/agent-skills/tsentials-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ RuleEngine.andTypedAsync<TContext, TResult>(...rules: TypedAsyncRule<TContext, T
RuleEngine.orTypedAsync<TContext, TResult>(...rules: TypedAsyncRule<TContext, TResult>[])
```

All three typed combinators return `Result.success(value)` when their rules pass: `andTyped`/`andTypedAsync` carry the *last passing* rule's value, `orTyped`/`orTypedAsync` carry the value of the *first passing* rule, and `linearTyped`/`linearTypedAsync` carry the *last* rule's value once the whole sequence passes. Zero rules is a special case per combinator: `linearTyped`/`linearTypedAsync` return a `RuleEngine.Empty` failure; `andTyped`/`andTypedAsync` return `Result.success(undefined)` (no errors to collect); `orTyped`/`orTypedAsync` **throw** `Error('Result.failureFrom requires at least one error.')` — same as the untyped `or` — because there are no errors to report and no success to return.

---

## Domain Error Hierarchies with Rules
Expand Down Expand Up @@ -242,7 +244,7 @@ if (!result.ok) {
```typescript
import { fromAsync } from 'tsentials/result';

// evaluate() is sync — use Result.then to chain into a pipeline
// evaluate() is sync — call it inside .andThen() to fold it into the async pipeline
const response = await fromAsync(fetchUser(userId))
.andThen(user => RuleEngine.evaluate(canRegister, user))
.match(
Expand Down
38 changes: 38 additions & 0 deletions .well-known/agent-skills/tsentials-string/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: tsentials-string
description: Use when converting string casing — toPascalCase/toCamelCase/toKebabCase/toSnakeCase/toMacroCase/toTrainCase/toTitleCase/toUnderscoreCamelCase, all word-boundary aware (camelCase, PascalCase, consecutive-uppercase runs like "XMLParser", whitespace, hyphens, underscores).
---

# tsentials/string — Skill

Use when converting between naming conventions (camelCase, kebab-case, snake_case, etc.).

## API

```typescript
import {
toPascalCase,
toCamelCase,
toKebabCase,
toSnakeCase,
toMacroCase,
toTrainCase,
toTitleCase,
toUnderscoreCamelCase,
} from 'tsentials/string';

toPascalCase('hello-world'); // "HelloWorld"
toCamelCase('hello-world'); // "helloWorld"
toKebabCase('helloWorld'); // "hello-world"
toSnakeCase('helloWorld'); // "hello_world"
toMacroCase('helloWorld'); // "HELLO_WORLD"
toTrainCase('hello_world'); // "Hello-World"
toTitleCase('helloWorld'); // "Hello World"
toUnderscoreCamelCase('helloWorld'); // "_helloWorld"
```

## Patterns

- Every function accepts any input casing (camelCase, PascalCase, kebab-case, snake_case, space-separated) — word boundaries are detected automatically, so there is no need to normalize input first.
- Consecutive uppercase runs split before the trailing capitalized word: `"XMLParser"` → `["XML", "Parser"]`.
- All functions are pure and take a single `string` argument — no options object, no locale handling.
27 changes: 17 additions & 10 deletions .well-known/agent-skills/tsentials-union/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,26 @@ type PaymentResult = Union<{

## Creating Values

Assign a fresh literal with `as`, not `:` — annotating the *variable* with `: PaymentResult` narrows it down to whichever member the literal matches, which breaks `Union.match` exhaustiveness the moment you use that variable:

```typescript
const result = { tag: 'success', value: { transactionId: 'txn_123' } } as PaymentResult;
const pending = { tag: 'pending', value: { estimatedMs: 3000 } } as PaymentResult;
const failed = { tag: 'failed', value: { error: Err.unexpected('Pay.Failed', 'Payment failed.') } } as PaymentResult;
```

`Union.of(tag, value)` also constructs a value, but its type parameter can't be inferred from a `Union<T>` alias — you must spell out the full record shape `T` explicitly:

```typescript
// Union.of(tag, value)
const result: PaymentResult = Union.of<PaymentResult extends Union<infer T> ? T : never, 'success'>(
'success',
{ transactionId: 'txn_123' },
);

// Or assign directly (the shape is { tag, value })
const result: PaymentResult = { tag: 'success', value: { transactionId: 'txn_123' } };
const pending: PaymentResult = { tag: 'pending', value: { estimatedMs: 3000 } };
const failed: PaymentResult = { tag: 'failed', value: { error: Err.unexpected('Pay.Failed', 'Payment failed.') } };
const created = Union.of<{
success: { transactionId: string };
pending: { estimatedMs: number };
failed: { error: AppError };
}, 'success'>('success', { transactionId: 'txn_123' });
```

The `as` form above is shorter and equally type-safe — prefer it unless you specifically need `Union.of`.

---

## Exhaustive Pattern Match
Expand Down
5 changes: 4 additions & 1 deletion .well-known/agent-skills/tsentials/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: tsentials
description: Master skill for tsentials — railway-oriented programming library for TypeScript. Load this skill first when working with any tsentials module. Contains critical naming rules, API patterns, decision trees for module selection, and references to all 18 module-specific skills.
description: Master skill for tsentials — railway-oriented programming library for TypeScript. Load this skill first when working with any tsentials module. Contains critical naming rules, API patterns, decision trees for module selection, and references to all 19 module-specific skills.
---

# tsentials — Master Skill
Expand Down Expand Up @@ -33,6 +33,7 @@ These naming rules MUST be followed. Getting them wrong causes runtime bugs.
| RequestBuilder terminal | `.fetchResult<T>()` | `.send<T>()` | Terminal method is `send`, not `fetchResult` |
| deepClone mechanism | "calls .clone()" | uses `structuredClone()` | `cloneArray` calls `.clone()`, `deepClone` uses structuredClone |
| Predicate.and arity | `Predicate.and(a, b, c)` | `Predicate.and(Predicate.and(a, b), c)` | `and`/`or` take exactly 2 args. Use `all`/`any` for variadic |
| String case functions | `toCamelCase(str, opts)` | `toCamelCase(str)` | Single-argument, no options object — normalization is automatic |

## Core API Patterns

Expand Down Expand Up @@ -137,6 +138,7 @@ RuleEngine.evaluateAsync(rule, ctx) // async → Promise<VoidResult>
| Structural equality | `eq` | `tsentials-eq` |
| Type-safe ordering/sorting | `ord` | `tsentials-ord` |
| Composable boolean predicates | `predicate` | `tsentials-predicate` |
| Convert between naming conventions | `string` | `tsentials-string` |
| Module overview / all imports | — | `tsentials-meta` |

## Design Principles
Expand Down Expand Up @@ -172,4 +174,5 @@ import { SystemDateTimeProvider, createFakeDateTimeProvider } from 'tsentials/ti
import type { DateTimeProvider } from 'tsentials/time';
import { deepClone, cloneArray } from 'tsentials/clone';
import { safeJsonParse, parseAndValidate } from 'tsentials/json';
import { toPascalCase, toCamelCase, toKebabCase, toSnakeCase } from 'tsentials/string';
```
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![npm version](https://img.shields.io/npm/v/tsentials?style=flat-square&color=blue)](https://www.npmjs.com/package/tsentials)
[![npm downloads](https://img.shields.io/npm/dm/tsentials?style=flat-square)](https://www.npmjs.com/package/tsentials)
[![bundle size](https://img.shields.io/bundlephobia/minzip/tsentials?style=flat-square&label=gzip)](https://bundlephobia.com/package/tsentials)
[![tests](https://img.shields.io/badge/tests-1079%20passing-brightgreen?style=flat-square)](./tests)
[![tests](https://img.shields.io/badge/tests-1089%20passing-brightgreen?style=flat-square)](./tests)
[![CI](https://img.shields.io/github/actions/workflow/status/senrecep/tsentials/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/senrecep/tsentials/actions)
[![license](https://img.shields.io/github/license/senrecep/tsentials?style=flat-square)](./LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0%2B-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ <h1><span>tsentials</span></h1>
<img src="https://img.shields.io/npm/v/tsentials?style=flat-square&color=blue" alt="npm version" />
<img src="https://img.shields.io/npm/dm/tsentials?style=flat-square" alt="npm downloads" />
<img src="https://img.shields.io/bundlephobia/minzip/tsentials?style=flat-square&label=gzip" alt="bundle size" />
<img src="https://img.shields.io/badge/tests-1079%20passing-brightgreen?style=flat-square" alt="tests" />
<img src="https://img.shields.io/badge/tests-1089%20passing-brightgreen?style=flat-square" alt="tests" />
<img src="https://img.shields.io/github/actions/workflow/status/senrecep/tsentials/ci.yml?branch=main&style=flat-square&label=CI" alt="CI" />
<img src="https://img.shields.io/badge/TypeScript-5.0%2B-blue?style=flat-square&logo=typescript" alt="TypeScript" />
<img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat-square&logo=node.js" alt="Node.js" />
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tsentials",
"version": "0.1.11",
"version": "0.1.12",
"description": "Railway-oriented programming for TypeScript — Result<T>, Maybe<T>, Rule Engine, and DDD base classes with full async pipeline support",
"type": "module",
"main": "./dist/index.js",
Expand Down
Loading
Loading