npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mantlebee/ts-core

v1.5.0

Published

Common types, patterns and utilities for TypeScript projects.

Readme

TypeScript Core (@mantlebee/ts-core)

Common types, patterns, and utilities for TypeScript projects.

A comprehensive library providing foundational components, design patterns, and utility functions to establish a uniform approach to developing TypeScript applications. This package emphasizes code reusability, maintainability, and consistent architecture across projects.

Table of Contents

Features

  • 🏗️ Design Patterns: Builder, Factory, and Wizard patterns
  • 🔔 Event Management: Typed event buses and subscribable objects
  • ⏱️ Time Management: Alarms, delays, and scheduling capabilities
  • 🔁 Async Control: Debounced and abortable delegates ("the latest call wins"), async lock ("one at a time")
  • 📝 Logging: Flexible logging system with multiple output strategies
  • 🎨 Color Utilities: Comprehensive color manipulation and conversion
  • 🔑 Identity Management: Numeric and GUID-based identity generators
  • 🧩 Type System: Rich TypeScript types for common scenarios
  • 📦 Utility Functions: Array, object, string, and value manipulation helpers
  • ⚙️ Tokenization: Text parsing and tokenization framework
  • 🎭 Wizard Workflow: State-based step-by-step workflow management
  • 🐛 Debug Support: Toggle debug/release mode for conditional behavior
  • ✅ Validation: Asynchronous validators for strings, numbers, emails, URLs, IPv4 addresses and passwords
  • 💾 Storage & Cache: In-memory and Web Storage backends, expiring caches
  • 🌐 HTTP: Pluggable HTTP client on top of the Fetch API

Installation

npm install @mantlebee/ts-core

Or with yarn:

yarn add @mantlebee/ts-core

Upgrading

Version 1.0.0 contains breaking changes (Subscribable, SubscribableLogger, Exception, no deep imports). See the 1.0.0 release notes for the migration guide. Version 1.1.0 fixes bugs that may change behaviour: see the 1.1.0 release notes. Version 1.2.0 adds abort() and isPending to DebouncedDelegate: see the 1.2.0 release notes. Version 1.2.1 makes DebouncedDelegate work in Node (no window dependency): see the 1.2.1 release notes. Version 1.3.0 adds AbortableDelegate and AsyncLock: see the 1.3.0 release notes. Version 1.3.1 makes AbortableDelegate work when read through a proxy: see the 1.3.1 release notes. Version 1.4.0 serializes query string lists as repeated keys, rejects objects and makes the serializer pluggable: see the 1.4.0 release notes. Version 1.5.0 adds JsonValue, IsoDateTime and their checks, ships ESM type declarations (.d.mts) and compiles to ES2020 (browsers since 2020, Node.js 14+): see the 1.5.0 release notes.

Quick Start

import {
  EventBus,
  ConsoleLogger,
  NumericIdentityManager,
  Color
} from '@mantlebee/ts-core';

// Create a typed event bus
const bus = new EventBus<{ message: [text: string] }>();
bus.on('message', (text) => console.log('Received:', text));
bus.trigger('message', 'Hello World');

// Use the console logger
const logger = new ConsoleLogger();
logger.logInfo('Application started');

// Generate unique IDs
const idManager = new NumericIdentityManager();
const id1 = idManager.newValue(); // 1
const id2 = idManager.newValue(); // 2

// Create and manipulate colors
const color = new Color(255, 128, 64);
console.log(color.hex()); // #ff8040
console.log(color.rgb()); // rgb(255,128,64)

Core Concepts

Common Types & Utilities

The common module provides fundamental types and utility functions used throughout the library.

Types

| Type | Description | | --- | --- | | List<T> | Array type alias | | Dictionary<TValue, TKey> | Object mapping keys (strings by default) to TValue | | Nullable<T> | T or null | | Undefinable<T> | T or undefined | | Any | Documented any alias | | KeyOf<T> | Union of all keys of type T | | ConstructorOf<T> | Constructor function type | | ValueOrGetter<TValue, TArg> | A value, or a function returning it | | JsonValue | A value that survives a JSON.stringify / JSON.parse round trip; check it with isJsonValue | | IsoDateTime | ISO 8601 date time in UTC (YYYY-MM-DDTHH:mm:ss.sssZ, milliseconds optional); check it with isIsoDateTime |

Utility Functions

Array Utilities:

import { firstOrDefault, listToDictionary, replaceListItems } from '@mantlebee/ts-core';

// Find first item or get default
const item = firstOrDefault([1, 2, 3], a => a === 2); // 2
const notFound = firstOrDefault([1, 2, 3], a => a === 5, -1); // -1

// Convert array to dictionary
const users = [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}];
const userDict = listToDictionary(users, 'id');
// { 1: {id: 1, name: 'John'}, 2: {id: 2, name: 'Jane'} }

// Replace array items in-place
const list = [1, 2, 3];
const ref = list;
replaceListItems(list, [4, 5, 6]);
// ref is now [4, 5, 6]

Object Utilities:

import { isObject, objectHasKey } from '@mantlebee/ts-core';

isObject({}) // true
isObject([]) // false
isObject(null) // false

objectHasKey({name: 'John'}, 'name') // true
objectHasKey({name: 'John'}, 'age') // false

JSON, Date Time and HTML Utilities:

import { escapeHtml, isIsoDateTime, isJsonValue, JsonValue } from '@mantlebee/ts-core';

// Recursive check of JSON-serializable values (narrows unknown to JsonValue)
const payload: unknown = { tags: ['a', 'b'], count: 2 };
if (isJsonValue(payload)) {
  const json: JsonValue = payload;
}
isJsonValue({ at: new Date() }) // false: dates, undefined, NaN, functions and cycles are rejected

// ISO 8601 in UTC, shape and calendar
isIsoDateTime('2024-02-29T23:59:59.999Z') // true
isIsoDateTime('2023-02-29T00:00:00Z')     // false, not a leap year
isIsoDateTime('2026-01-01T00:00:00+01:00') // false, only Z is accepted

// Escape & < > " ' before inserting text into HTML
escapeHtml(`<b title="x">Tom & 'Jerry'</b>`)
// "&lt;b title=&quot;x&quot;&gt;Tom &amp; &#39;Jerry&#39;&lt;/b&gt;"

String, Number, and Date Utilities: The library includes utilities for string manipulation, number formatting, character handling, boolean parsing, and date calculations.

Builders & Factories

Implement the Builder and Factory design patterns for flexible object construction.

Builder Pattern

The Builder class uses a factory to create objects with support for debug/release modes:

import { Builder, IFactory } from '@mantlebee/ts-core';

// Define your factory
class MyObjectFactory implements IFactory<MyObject> {
  createDebug(): MyObject {
    return new MyObject({ verbose: true });
  }
  
  createRelease(): MyObject {
    return new MyObject({ verbose: false });
  }
}

// Use the builder
const builder = new Builder(new MyObjectFactory());
const obj = builder.build(); // Creates debug or release version based on DebugMode

Factory Pattern

The Factory interface allows you to define creation strategies:

import { IFactory } from '@mantlebee/ts-core';

const factory: IFactory<string> = {
  createDebug: () => 'Debug Version',
  createRelease: () => 'Release Version'
};

Subscriptions & Observables

Publish/subscribe with typed events. Events are declared as an { eventName: argsTuple } map, so handlers and triggers are checked at compile time.

Event Bus

import { EventBus } from '@mantlebee/ts-core';

type UserEvents = { updated: [user: { id: number; name: string }] };

const userUpdates = new EventBus<UserEvents>();

// Subscribe to an event
const subscription = userUpdates.on('updated', (user) => {
  console.log(`User updated: ${user.name}`);
});

// Notify all handlers of the event
userUpdates.trigger('updated', { id: 1, name: 'Alice' });

// Unsubscribe
await subscription.dispose();

Subscribable

Extend Subscribable to expose only on() to the outside, while the class raises its own events:

import { Subscribable } from '@mantlebee/ts-core';

type CounterEvents = { changed: [value: number] };

class Counter extends Subscribable<CounterEvents> {
  private value = 0;
  public increment(): void {
    this._eventBus.trigger('changed', ++this.value);
  }
}

const counter = new Counter();
counter.on('changed', (value) => console.log(value));
counter.increment(); // 1
await counter.dispose(); // removes every handler

Key Features:

  • Type-safe events and arguments
  • Multiple events per bus, multiple handlers per event
  • Disposable subscriptions
  • MultiTabEventBus to deliver events to the other browser tabs

Delays & Alarms

Schedule and manage time-based operations with Alarm.

import { Alarm } from '@mantlebee/ts-core';

// Create an alarm for a future date
const futureDate = new Date(Date.now() + 5000); // 5 seconds from now

const alarm = new Alarm(futureDate, (stop, snooze) => {
  console.log('Alarm triggered!');
  
  // You can stop the alarm
  stop();
  
  // Or snooze it for another 3 seconds
  // snooze(3000);
});

// Check alarm status
console.log(alarm.expired); // false (until 5 seconds pass)
console.log(alarm.stopped); // false

// Manually stop the alarm
alarm.stop();

// Snooze the alarm
alarm.snooze(2000); // Delay execution by 2 more seconds

Alarm Properties:

  • expired: Check if the alarm time has passed
  • stopped: Check if the alarm has been stopped
  • snooze(ms): Delay execution by specified milliseconds
  • stop(): Stop the alarm from executing

Delegates

Manage function execution with specialized delegate types.

Debounced Delegate

Prevent multiple rapid function executions:

import { DebouncedDelegate } from '@mantlebee/ts-core';

const saveToServer = (data: string) => {
  console.log('Saving:', data);
};

// Create a debounced version (300ms delay). The delegate receives the
// arguments of the last call as `unknown`.
const debouncedSave = new DebouncedDelegate(
  (data) => saveToServer(data as string),
  300
);

// Simulate rapid calls
debouncedSave.call('v1');
debouncedSave.call('v2');
debouncedSave.call('v3'); // Only this one will execute after 300ms

Use Cases:

  • Search input handling
  • Auto-save functionality
  • Window resize handlers
  • API call throttling

Abortable Delegate

Run an async function so that the latest call wins. Each call() aborts the running call: its AbortSignal is aborted and its promise rejects with AbortedException right away, without waiting for its function to end. When the function of a superseded call settles later, its result is discarded and its rejection is swallowed, so even a reader that cannot cancel its request is safe to wrap. The delegate tells the latest call apart by a sequence number, not by object identity, so it behaves the same when the instance is read through a proxy that wraps the objects it returns.

import { AbortableDelegate, isAbortedException } from '@mantlebee/ts-core';

// The function receives the signal first, then the arguments of the call.
const search = new AbortableDelegate(async (signal, query: string) => {
  // Cooperative: the signal is passed to fetch, so the request is cancelled.
  const response = await fetch(`/api/users?q=${query}`, { signal });
  const users: User[] = await response.json();
  // Apply the result only while this call is still the latest one.
  if (!signal.aborted) render(users);
  return users;
});

// Superseded by the next call: each rejects with AbortedException, which must
// be handled like any other rejection.
search.call('t').catch(() => undefined);
search.call('ts').catch(() => undefined);
const users = await search.call('tsc'); // the result of 'tsc'

// A caller that awaits a call ignores the rejection of a superseded one.
try {
  await search.call('type');
} catch (error) {
  if (!isAbortedException(error)) throw error;
}

A function that cannot cancel its work checks the signal after each await and skips its side effects:

const load = new AbortableDelegate(async (signal, id: number) => {
  const item = await repository.get(id); // no cancellation support
  if (signal.aborted) return item;       // superseded: do not touch the state
  selected = item;
  return item;
});

A superseded call rejects: it does not resolve with the result of the call that replaced it, because its caller awaited a result for its arguments. abort() aborts the running call with the same rejection; isRunning is true from the first call() until the latest call settles. A call() made from inside the function supersedes the outer call like any other.

AbortableDelegate does not debounce. Compose it with DebouncedDelegate when a quiet time is wanted: type, wait, then the latest search wins.

import { AbortableDelegate, DebouncedDelegate } from '@mantlebee/ts-core';

const search = new AbortableDelegate(async (signal, query: string) => {
  const response = await fetch(`/api/users?q=${query}`, { signal });
  if (!signal.aborted) render(await response.json());
});
const debouncedSearch = new DebouncedDelegate(
  (query) => search.call(query as string).catch(() => undefined),
  300
);

input.addEventListener('input', () => debouncedSearch.call(input.value));

Use Cases:

  • Search-as-you-type where only the latest query matters
  • A data source whose read() must expose the latest request only
  • Reloading a view while a previous load is in flight
  • Pages of a virtual scroller superseded by a new filter

Async Lock

Run async functions one at a time on a shared resource. AsyncLock is an exclusive, non-reentrant, FIFO lock: run(fn) runs fn once every function enqueued before it has settled, and resolves or rejects with the outcome of fn. A failure releases the lock like a success does, so the next function starts anyway.

import { AsyncLock } from '@mantlebee/ts-core';

const lock = new AsyncLock();

// Both reads write the same filter of the same source: they never overlap.
const suggestions = lock.run(async () => {
  source.filter = { name: { contains: 'a' } };
  return source.read();
});
const selected = lock.run(async () => {
  source.filter = { id: { equal: 42 } };
  return source.read();
});

console.log(lock.isLocked);     // true: the first read is running
console.log(lock.pendingCount); // 1: the second read is waiting

Combined with AbortableDelegate, a call that was superseded while waiting for the lock checks its signal as soon as it starts and returns at once. A combobox, for example, shares one lock between its three kinds of reads and gives each kind its own delegate:

import { AbortableDelegate, AsyncLock } from '@mantlebee/ts-core';

const lock = new AsyncLock();
const search = new AbortableDelegate(async (signal, query: string) =>
  lock.run(async () => {
    if (signal.aborted) return; // superseded while waiting: nothing to do
    applyQueryFilter(query);
    await dataSource.read();
    if (!signal.aborted) suggestions = dataSource.items;
  })
);
const resolveValue = new AbortableDelegate(async (signal, value: number) =>
  lock.run(async () => {
    if (signal.aborted) return;
    applyValueFilter(value);
    await dataSource.read();
    removeValueFilter();
    if (!signal.aborted) selected = dataSource.items[0];
  })
);

The lock is not reentrant: a run() called from inside a running function waits for that function, which waits for the inner run(), so it never completes. Run the inner work directly instead.

Use Cases:

  • Several readers writing the same filter of one data source
  • Serializing writes to a file, a storage key or a connection
  • Queueing the pages of a scroller behind the search that produced them
  • Making an initialization step run once before its dependants

Identities & Identity Management

Generate and manage unique identifiers.

Numeric Identity Manager

Generate sequential numeric IDs:

import { NumericIdentityManager } from '@mantlebee/ts-core';

const idManager = new NumericIdentityManager();
console.log(idManager.newValue()); // 1
console.log(idManager.newValue()); // 2
console.log(idManager.lastValue);  // 2

// Start from a specific number
const manager = new NumericIdentityManager(100);
console.log(manager.newValue()); // 101

GUID Identity Manager

Generate unique GUIDs:

import { GuidIdentityManager } from '@mantlebee/ts-core';

const guidManager = new GuidIdentityManager();
const id1 = guidManager.newValue(); // e.g., "550e8400-e29b-41d4-a716-446655440000"
const id2 = guidManager.newValue(); // Different GUID
console.log(guidManager.lastValue);

Typed Keys

Create type-safe keys with identity:

import { createTypedKey, TypedKey } from '@mantlebee/ts-core';

interface User {
  id: number;
  name: string;
}

// A unique Symbol, tagged at the type level with the type it stands for
const userKey: TypedKey<User> = createTypedKey<User>();

Loggers

Implement flexible logging with multiple strategies.

Logger Interface

import { Logger, LogTypes, ILogger } from '@mantlebee/ts-core';

// Create a custom logger
const customLogger = new Logger((type, message, data) => {
  console.log(`[${type}] ${message}`, data);
});

customLogger.logInfo('User logged in');
customLogger.logError('Connection failed', { error: 'TIMEOUT' });
customLogger.logDebug('Variable state', { var: 123 });
customLogger.logWarning('Deprecated API used');
customLogger.logSuccess('Operation completed');

Console Logger

Pre-configured logger for console output:

import { ConsoleLogger } from '@mantlebee/ts-core';

const logger = new ConsoleLogger();
logger.logInfo('Starting application');
logger.logError('An error occurred');

Subscribable Logger

Listen to log events:

import { SubscribableLogger, LogTypes } from '@mantlebee/ts-core';

const logger = new SubscribableLogger();

logger.on('log', ({type, message, data}) => {
  if (type === LogTypes.error) {
    sendToErrorTracker(message, data);
  }
});

logger.logError('Critical error', { code: 500 });

Log Types:

  • debug - Debug information
  • info - General information
  • success - Success messages
  • warning - Warning messages
  • error - Error messages

Wizards

Manage complex multi-step workflows with state and validation.

import { Wizard, IWizardStep } from '@mantlebee/ts-core';

// Define wizard steps: each one says whether the wizard can leave it
const steps: IWizardStep[] = [
  { canGoBack: false, canGoForward: true },
  { canGoBack: true, canGoForward: true },
  {
    canGoBack: true,
    canGoForward: false,
    beforeEnter: async () => console.log('Last step')
  }
];

// Create wizard context
const context = {
  canAbort: true,
  canComplete: true,
  abort: () => Promise.resolve(),
  complete: () => Promise.resolve()
};

// Initialize wizard
const wizard = new Wizard(context, steps);

// Navigate through steps
await wizard.start();

// Check current state
console.log(wizard.step);           // Current step
console.log(wizard.status);         // Current status
console.log(wizard.canGoForward);   // Can advance?
console.log(wizard.canGoBack);      // Can go back?

await wizard.goForward();
await wizard.goBack();
await wizard.complete();

Wizard Statuses:

  • needToStart - Wizard not yet started
  • starting - Being started
  • idle - Ready for action
  • goingForward / goingBack - Moving to another step
  • aborting - Being aborted
  • aborted - Aborted state
  • completing - Being completed
  • completed - Completed state

Enablable Base Class:

Common base class for things that can be enabled/disabled:

import { Enablable } from '@mantlebee/ts-core';

const feature = new Enablable(false);
console.log(feature.isEnabled); // false

feature.enable();
console.log(feature.isEnabled); // true

feature.disable();
console.log(feature.isEnabled); // false

Colors

Comprehensive color manipulation and format conversion.

import { Color } from '@mantlebee/ts-core';

// Create a color with RGBA values
const color = new Color(255, 128, 64, 1);

// Access individual channels
console.log(color.red);   // 255
console.log(color.green); // 128
console.log(color.blue);  // 64
console.log(color.alpha); // 1

// Convert to different formats
console.log(color.hex());    // #ff8040
console.log(color.rgb());    // rgb(255,128,64)
console.log(color.rgba());   // rgba(255,128,64,1)
console.log(color.hsl());    // hsl(20,100%,63%)
console.log(color.hsla());   // hsla(20,100%,63%,1)

// Get contrasting color for readability
const contrast = color.contrast(); // Returns white or black Color instance

// Create from existing formats
const fromHex = Color.fromHex('#ff8040');
const fromRgb = Color.fromRgb('rgb(255,128,64)');
const fromHsl = Color.fromHsl('hsl(20,100%,63%)');
const fromName = Color.fromWebColorName('red');

Supported Formats:

  • Hex (#RRGGBB, #RRGGBBAA)
  • RGB/RGBA (rgb(), rgba())
  • HSL/HSLA (hsl(), hsla())
  • Web color names (red, blue, green, etc.)

Parsers & Tokenizers

Parse and tokenize text with flexible rule-based system.

import { Tokenizer, TokenizerRule } from '@mantlebee/ts-core';

// A rule decides whether something matches at a given position
const regexRule = (regex: RegExp, value: string): TokenizerRule => ({
  match(text, index) {
    const sticky = new RegExp(regex.source, 'y');
    sticky.lastIndex = index;
    const found = sticky.exec(text);
    return found
      ? { endIndex: index + found[0].length - 1, match: found[0], value }
      : null;
  }
});

// Create tokenizer: rules are tried in order at each position
const tokenizer = new Tokenizer([
  regexRule(/\d+/, 'number'),
  regexRule(/[+\-*\/]/, 'operator')
]);

// Tokenize text
const { tokens } = tokenizer.tokenize('1 + 2 * 1');
// [
//   { match: '1', value: 'number', occurrences: [
//     { startIndex: 0, endIndex: 0 }, { startIndex: 8, endIndex: 8 }
//   ] },
//   { match: '+', value: 'operator', occurrences: [{ startIndex: 2, endIndex: 2 }] },
//   { match: '2', value: 'number', occurrences: [{ startIndex: 4, endIndex: 4 }] },
//   { match: '*', value: 'operator', occurrences: [{ startIndex: 6, endIndex: 6 }] }
// ]

Tokenizer Features:

  • Rule-based matching: a rule is any object with a match(text, index) method
  • Rules applied in order, the first match wins
  • Positions no rule matches are skipped
  • Repeated matches collapsed into one token with all its occurrences

Scheduling

Define and manage schedule patterns for recurring events.

import { 
  OneTimeSchedule, 
  DailySchedule, 
  WeeklySchedule,
  MonthlyByDaySchedule,
  WeekDays,
  Months
} from '@mantlebee/ts-core';

// One-time execution
const oneTime: OneTimeSchedule = {
  startDate: new Date('2024-08-25')
};

// Daily execution
const daily: DailySchedule = {
  startDate: new Date(),
  every: 3, // Every 3 days
  repeat: { every: { hours: 0, minutes: 30 } } // Repeat every 30 minutes
};

// Weekly execution
const weekly: WeeklySchedule = {
  startDate: new Date(),
  days: [WeekDays.monday, WeekDays.wednesday, WeekDays.friday],
  every: 2 // Every 2 weeks
};

// Monthly execution by day
const monthlyByDay: MonthlyByDaySchedule = {
  startDate: new Date(),
  days: [5, 15, 25], // 5th, 15th, 25th of each month
  months: [Months.january, Months.february]
};

Schedule Types:

  • OneTimeSchedule - Execute once at specific date/time
  • DailySchedule - Execute every X days
  • WeeklySchedule - Execute on specific days of the week
  • MonthlyByDaySchedule - Execute on specific days of months
  • MonthlyByWeeklyDaySchedule - Execute on specific weekly days (e.g., first Monday)

Calendar

Calendar constants and utilities.

import { WeekDays, Months, MonthWeekConditions } from '@mantlebee/ts-core';

const weekDay = WeekDays.monday;
const month = Months.january;
const condition = MonthWeekConditions.first; // First occurrence

Exceptions

Custom exception handling with codes. Every exception of the library extends BaseException (an Error with an optional code and cause); Exception is the variant with a required code.

import { Exception } from '@mantlebee/ts-core';

// Create custom exceptions
class ValidationException extends Exception {
  constructor(message: string) {
    super('VALIDATION_ERROR', message);
    this.name = 'ValidationException'; // class names don't survive minification
  }
}

// Throw and catch
try {
  throw new ValidationException('Invalid email format');
} catch (error) {
  if (error instanceof Exception && error.code === 'VALIDATION_ERROR') {
    console.log('Validation failed:', error.message);
  }
}

Debug Mode

Toggle debug/release mode globally for conditional behavior.

import { DebugMode } from '@mantlebee/ts-core';

// Check current state
console.log(DebugMode.isEnabled); // false by default

// Enable debug mode
DebugMode.enable();
console.log(DebugMode.isEnabled); // true

// Disable debug mode
DebugMode.disable();

// Use in your code
if (DebugMode.isEnabled) {
  console.log('Detailed debug information');
} else {
  console.log('Production mode');
}

Integration with Builder:

import { Builder, DebugMode } from '@mantlebee/ts-core';

DebugMode.enable();
const debugObj = builder.build(); // Uses createDebug()

DebugMode.disable();
const releaseObj = builder.build(); // Uses createRelease()

Validators, Storage, Cache & More

Version 1.0 adds validators, storages, caches, an HTTP client, notifications, clipboard and file helpers. Each folder under src/ has a README.md describing its content.

Validators

Validators are asynchronous and return the keys of the failed checks, ready to be translated:

import { EmailValidator, PasswordValidator } from '@mantlebee/ts-core';

const email = new EmailValidator({ required: true });
await email.validate('');          // { isValid: false, messages: ['empty'] }
await email.validate('[email protected]');   // { isValid: true }

// Default policy: 8-30 chars, lowercase, uppercase, number, symbol, no spaces
const password = new PasswordValidator({ required: true });
await password.validate('Secret!1');  // { isValid: true }

Empty values are valid unless required is true.

Storage & Cache

import { InMemoryExpirableCache, LocalStorage } from '@mantlebee/ts-core';

// JSON-serialized values in localStorage (browser only)
const storage = new LocalStorage();
storage.set('settings', { theme: 'dark' });
storage.get<{ theme: string }>('settings'); // { theme: 'dark' }

// Values expiring 60 seconds after their last write
const cache = new InMemoryExpirableCache<string, number>(60);
cache.write(1, 'Alice');
cache.read(1); // 'Alice', or null once expired

API Reference

For detailed API documentation, please visit: https://mantlebee.github.io/ts-core/

Examples

Example 1: Event-Driven Logger

import { SubscribableLogger, EventBus, LogTypes } from '@mantlebee/ts-core';

const logger = new SubscribableLogger();
const alerts = new EventBus<{ error: [message: string] }>();

// Forward errors to alert system
logger.on('log', ({type, message}) => {
  if (type === LogTypes.error) {
    alerts.trigger('error', message);
  }
});

// Subscribe to alerts
alerts.on('error', (message) => {
  console.error('🚨 ALERT:', message);
  // Send to error tracking service
});

// Use the logger
logger.logError('Database connection failed');

Example 2: Building with Factory Pattern

import { Builder, IFactory } from '@mantlebee/ts-core';

class ApiClient {
  constructor(private debug: boolean) {}
  
  async fetch(url: string) {
    if (this.debug) console.log('Fetching:', url);
    return fetch(url);
  }
}

class ApiFactory implements IFactory<ApiClient> {
  createDebug() { return new ApiClient(true); }
  createRelease() { return new ApiClient(false); }
}

const builder = new Builder(new ApiFactory());
const client = builder.build();

Example 3: Debounced Search

import { DebouncedDelegate } from '@mantlebee/ts-core';

const searchAPI = (query: string) => {
  console.log('Searching for:', query);
  // Perform actual search
};

const debouncedSearch = new DebouncedDelegate(
  (query) => searchAPI(query as string),
  500
);

// Simulate user typing
const input = 'typescript';
input.split('').forEach((char, i) => {
  setTimeout(() => {
    debouncedSearch.call(input.substring(0, i + 1));
  }, 100 * i);
});
// Only one search call will be made after user stops typing

Example 4: Latest Read Wins

A data source whose read() exposes the latest request only: items comes from the read requested last, not from the one that resolves last, and isReading stays true until that read settles.

import { AbortableDelegate, isAbortedException } from '@mantlebee/ts-core';

class DataSource<TItem> {
  public items: TItem[] = [];
  private readonly _read = new AbortableDelegate(async (signal) => {
    const items = await this.reader(this.buildPayload(), signal);
    // A reader that ignores the signal still resolves: assign only while this
    // read is the latest one.
    if (!signal.aborted) this.items = items;
    return items;
  });

  public constructor(
    private readonly reader: (payload: unknown, signal: AbortSignal) => Promise<TItem[]>
  ) {}

  public get isReading(): boolean {
    return this._read.isRunning;
  }
  /** Rejects with `AbortedException` when a newer read supersedes it. */
  public read(): Promise<TItem[]> {
    return this._read.call();
  }
  private buildPayload(): unknown {
    return {};
  }
}

const source = new DataSource((payload, signal) =>
  fetch('/api/items', { method: 'POST', body: JSON.stringify(payload), signal })
    .then((response) => response.json())
);

source.read().catch((error) => { if (!isAbortedException(error)) throw error; });
await source.read(); // the only read that assigns `items`

Example 5: Search Query with a Debounce

A search box: the query is debounced, then given to an abortable delegate, so that a new query supersedes the previous one and isSearching mirrors the latest call only.

import { AbortableDelegate, DebouncedDelegate, isAbortedException } from '@mantlebee/ts-core';

let results: string[] = [];

const search = new AbortableDelegate(async (signal, query: string) => {
  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
  const found: string[] = await response.json();
  if (!signal.aborted) results = found; // apply only when still the latest
});
const debounced = new DebouncedDelegate((query) => {
  search.call(query as string).catch((error) => {
    if (!isAbortedException(error)) console.error(error);
  });
}, 250);

const isSearching = () => search.isRunning;

debounced.call('t');
debounced.call('ts'); // only this query runs, 250ms after the last keystroke

Example 6: Multi-Step Workflow

import { Wizard, IWizardStep } from '@mantlebee/ts-core';

// A step can go forward once its data is valid
const createStep = (isValid: () => boolean): IWizardStep => ({
  canGoBack: true,
  get canGoForward() {
    return isValid();
  }
});

const steps: IWizardStep[] = [
  createStep(() => validateCredentials()),
  createStep(() => validateMFA()),
  createStep(() => validateProfile())
];

const wizard = new Wizard(
  {
    canAbort: true,
    canComplete: true,
    complete: () => saveUserData()
  },
  steps
);

// Navigate workflow
await wizard.start();
while (wizard.canGoForward) {
  await wizard.goForward();
}
if (wizard.canComplete) await wizard.complete();

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

MIT License - See LICENSE file for details


Repository: https://github.com/mantlebee/ts-core

Issues: https://github.com/mantlebee/ts-core/issues

Documentation: https://mantlebee.github.io/ts-core/