@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-coreOr with yarn:
yarn add @mantlebee/ts-coreUpgrading
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') // falseJSON, 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>`)
// "<b title="x">Tom & 'Jerry'</b>"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 DebugModeFactory 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 handlerKey Features:
- Type-safe events and arguments
- Multiple events per bus, multiple handlers per event
- Disposable subscriptions
MultiTabEventBusto 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 secondsAlarm Properties:
expired: Check if the alarm time has passedstopped: Check if the alarm has been stoppedsnooze(ms): Delay execution by specified millisecondsstop(): 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 300msUse 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 waitingCombined 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()); // 101GUID 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 informationinfo- General informationsuccess- Success messageswarning- Warning messageserror- 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 startedstarting- Being startedidle- Ready for actiongoingForward/goingBack- Moving to another stepaborting- Being abortedaborted- Aborted statecompleting- Being completedcompleted- 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); // falseColors
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/timeDailySchedule- Execute every X daysWeeklySchedule- Execute on specific days of the weekMonthlyByDaySchedule- Execute on specific days of monthsMonthlyByWeeklyDaySchedule- 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 occurrenceExceptions
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 expiredAPI 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 typingExample 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 keystrokeExample 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/
