@asaidimu/utils
TypeScript icon, indicating that this package has built-in type declarations

2.2.0 • Public • Published

@asaidimu/utils

A collection of high-quality TypeScript utilities for building robust and observable applications. This monorepo encapsulates various tools designed to enhance web development with features like advanced data persistence, class management with integrated telemetry, and an intelligent in-memory cache.

npm version License Build Status TypeScript


🚀 Quick Links


📦 Overview & Features

@asaidimu/utils is a comprehensive monorepo providing a suite of meticulously crafted TypeScript utilities. Each sub-package addresses common challenges in modern web application development, offering highly configurable, type-safe, and performant solutions. From managing data across browser tabs and persistent storage to enhancing class lifecycles with observability and creating intelligent caching layers, this collection aims to simplify complex tasks and boost developer productivity.

Designed with modularity and reusability in mind, these utilities can be seamlessly integrated into various front-end frameworks (React, Vue, Angular, Svelte) or used in vanilla JavaScript projects. The emphasis on robust error handling, cross-tab synchronization, and integrated telemetry ensures that applications built with these tools are not only powerful but also maintainable and transparent in their operation.

Key Features

  • Modular & Composable: A collection of independent utilities that can be used together or separately.
  • Type-Safe Development: Fully written in TypeScript, providing excellent autocompletion and compile-time error checking.
  • Cross-Browser Compatibility: Designed for modern web environments where applicable.
  • Performance & Efficiency: Optimized for speed and minimal overhead.
  • Built-in Observability: Includes features for monitoring and debugging application behavior, especially within the @asaidimu/utils/class and @asaidimu/utils/store packages.
  • Robust Error Handling: Each utility is built with resilience, handling common failure scenarios gracefully.
  • Extensible Design: Provides interfaces and patterns to allow for easy customization and extension.

🛠️ Installation & Setup

Prerequisites

To use @asaidimu/utils, you need:

  • Node.js: Version 18 or higher (LTS recommended)
  • npm or Yarn or Bun: A package manager for Node.js
  • A modern web browser environment (e.g., Chrome, Firefox, Safari, Edge) for client-side utilities.

Installation Steps

Install the entire collection using your preferred package manager. You can then import specific utilities as needed.

# Using npm
npm install @asaidimu/utils

# Using Yarn
yarn add @asaidimu/utils

# Using Bun
bun add @asaidimu/utils

Configuration

Each utility within @asaidimu/utils has its own specific configuration options, typically passed directly to its constructor or initialization function. There is no global configuration for the entire monorepo package beyond standard tsconfig.json settings for your project. Refer to the individual package documentation below for detailed configuration examples.

Verification

You can quickly verify the installation by attempting to import one of the modules:

import { WebStoragePersistence } from '@asaidimu/utils/persistence';
// import { createClass } from '@asaidimu/utils/class'; // Note: createClass is currently stubbed
import { Cache } from '@asaidimu/utils/cache';
import { ReactiveDataStore } from '@asaidimu/utils/store';

console.log('Core utilities loaded successfully!');

// You can now create instances:
// const localStorage = new WebStoragePersistence<{ foo: string }>('my-app-state');
// const MyClass = createClass(() => ({ value: 1 })); // Currently requires direct import from factory.ts
// const cache = new Cache();
// const store = new ReactiveDataStore({ count: 0 });

📦 Packages

This section provides detailed documentation for each utility package included in @asaidimu/utils.

@asaidimu/utils/persistence

@asaidimu/utils/persistence is a lightweight, type-safe library designed to simplify data persistence in modern web applications. It provides a unified API for interacting with various browser storage mechanisms, including localStorage, sessionStorage, IndexedDB, and an in-memory store. A core strength of this library is its built-in support for cross-instance synchronization, ensuring that state changes made in one browser tab, window, or even a logically separate component instance within the same tab, are automatically reflected and propagated to other active instances using the same persistence mechanism.

Key Features

  • Unified SimplePersistence<T> API: A consistent interface for all storage adapters, simplifying integration and making switching storage types straightforward.
  • Flexible Storage Options:
    • WebStoragePersistence: Supports both localStorage (default) and sessionStorage for simple key-value storage.
    • IndexedDBPersistence: Provides robust, high-capacity, and structured data storage for more complex needs like offline caching.
    • EphemeralPersistence: Offers an in-memory store with cross-tab synchronization using a Last Write Wins (LWW) strategy.
  • Automatic Cross-Instance Synchronization: Real-time updates across multiple browser tabs, windows, or even components within the same tab.
  • Type-Safe: Fully written in TypeScript, providing strong typing, compile-time checks, and autocompletion.
  • Lightweight & Minimal Dependencies: Designed to be small and efficient.
  • Robust Error Handling: Includes internal error handling for common storage operations.
  • Instance-Specific Subscriptions: The subscribe method intelligently uses a unique instanceId to listen for changes from other instances of the application, preventing self-triggered updates.

Installation

Already installed as part of @asaidimu/utils. Just import:

import { WebStoragePersistence, IndexedDBPersistence, EphemeralPersistence, type SimplePersistence } from '@asaidimu/utils/persistence';

Core Interface: SimplePersistence

Every persistence adapter in this library implements the SimplePersistence<T> interface, where T is the type of data you want to persist.

export interface SimplePersistence<T> {
  /**
   * Persists data to storage.
   * @param id The **unique identifier of the *consumer instance*** making the change. This is NOT the ID of the data.
   *           It should typically be a **UUID** generated once at the consumer instance's instantiation.
   * @param state The state (of type T) to persist.
   * @returns `true` if successful, `false` if an error occurred. For asynchronous implementations, returns a `Promise<boolean>`.
   */
  set(id: string, state: T): boolean | Promise<boolean>;

  /**
   * Retrieves the global persisted data from storage.
   * @returns The retrieved state of type `T`, or `null` if not found. For asynchronous implementations, returns a `Promise<T | null>`.
   */
  get(): (T | null) | (Promise<T | null>);

  /**
   * Subscribes to changes in the global persisted data that originate from *other* instances.
   * @param id The **unique identifier of the *consumer instance* subscribing**. This allows filtering out notifications from the subscribing instance itself.
   * @param callback The function to call when the global persisted data changes from *another* source.
   * @returns A function that, when called, will unsubscribe the provided callback.
   */
  subscribe(id: string, callback: (state: T) => void): () => void;

  /**
   * Clears the entire global persisted data from storage.
   * @returns `true` if successful, `false` if an error occurred. For asynchronous implementations, returns a `Promise<boolean>`.
   */
  clear(): boolean | Promise<boolean>;
}

Usage Examples

Web Storage Persistence (WebStoragePersistence)

Uses localStorage (default) or sessionStorage. Synchronous operations with asynchronous cross-tab sync.

import { WebStoragePersistence } from '@asaidimu/utils/persistence';
import { v4 as uuidv4 } from 'uuid'; // Recommended for generating unique instance IDs

interface UserPreferences { theme: 'dark' | 'light'; notificationsEnabled: boolean; }
const instanceId = uuidv4(); // Unique ID for this browser tab/instance

// 1. Using localStorage (default)
const userPrefsStore = new WebStoragePersistence<UserPreferences>('user-preferences');
userPrefsStore.set(instanceId, { theme: 'dark', notificationsEnabled: true });
console.log('Preferences set:', userPrefsStore.get());

// Subscribe to changes from other tabs
const unsubscribe = userPrefsStore.subscribe(instanceId, (newState) => {
  console.log('Preferences updated from another tab:', newState);
});

// 2. Using sessionStorage
const sessionDataStore = new WebStoragePersistence<{ lastVisitedPage: string }>('session-data', true); // Pass `true` for sessionStorage
sessionDataStore.set(instanceId, { lastVisitedPage: '/dashboard' });
console.log('Session data:', sessionDataStore.get());

// Don't forget to unsubscribe when done
// unsubscribe();
IndexedDB Persistence (IndexedDBPersistence)

Uses IndexedDB for robust, asynchronous storage. Ideal for larger or structured data.

import { IndexedDBPersistence } from '@asaidimu/utils/persistence';
import { v4 as uuidv4 } from 'uuid'; // Recommended for generating unique instance IDs

interface Product { id: string; name: string; price: number; stock: number; }

const productCache = new IndexedDBPersistence<Product[]>({
  store: 'all-products-cache',
  database: 'my-app-db',
  collection: 'products', // Object store name
});
const instanceId = uuidv4(); // Unique ID for this browser tab/instance

async function manageProductCache() {
  const products: Product[] = [
    { id: 'p001', name: 'Laptop', price: 1200, stock: 50 },
    { id: 'p002', name: 'Mouse', price: 25, stock: 200 },
  ];
  await productCache.set(instanceId, products);
  console.log('Products cached successfully.');

  const cachedProducts = await productCache.get();
  if (cachedProducts) {
    console.log('Retrieved products:', cachedProducts);
  }

  const unsubscribe = productCache.subscribe(instanceId, (newState) => {
    console.log('Product cache updated by another instance:', newState);
  });

  // Important: Close the underlying IndexedDB connection when your application is shutting down.
  // await productCache.close();
  // unsubscribe();
}

manageProductCache();
Ephemeral Persistence (EphemeralPersistence)

An in-memory store for transient, session-specific state that needs cross-tab synchronization via Last Write Wins (LWW) strategy, but not persistence across page reloads.

import { EphemeralPersistence } from '@asaidimu/utils/persistence';
import { v4 as uuidv4 } from 'uuid';

interface SessionCounter { count: number; lastUpdated: number; }
const instanceId = uuidv4();
const sessionCounterStore = new EphemeralPersistence<SessionCounter>('my-session-counter');

async function manageSessionCounter() {
  sessionCounterStore.set(instanceId, { count: 1, lastUpdated: Date.now() });
  console.log('Initial session count:', sessionCounterStore.get());

  const unsubscribe = sessionCounterStore.subscribe(instanceId, (newState) => {
    console.log('🔔 Session count updated from another instance:', newState);
  });
  // unsubscribe();
}

manageSessionCounter();

@asaidimu/utils/class

@asaidimu/utils/class is a versatile utility library designed to streamline class creation, manage instance lifecycles, and provide deep observability into your application's runtime behavior. At its core, it offers createClass, a highly configurable factory function that goes beyond standard class constructors, supporting robust singleton management, automatic cleanup, and a powerful observation mechanism.

Complementing createClass, the library provides TelemetryAdapter, a ready-to-use solution for integrating these observations with your observability stack (specifically Loki for logs and Mimir for metrics).

NOTE: As per the current codebase (src/class/index.ts), the createClass and TelemetryRegistry exports are temporarily stubbed. The examples below reflect their intended functionality based on factory.ts and observer.ts. For actual usage, you might need to import directly from factory.ts and observer.ts if the stub is active in your build process.

Key Features

🚀 createClass:

  • Flexible Class Instantiation: Create classes from plain object initializers (sync or async).
  • Automated Cleanup: Register cleanup callbacks for instances, integrating with JavaScript's FinalizationRegistry for automatic resource management.
  • Robust Singleton Patterns: Implement global singletons or parameter-based singletons using keySelector for dynamic instance caching.
  • Lifecycle & Usage Observation: Monitor critical events including: create, cleanup, access, method-call, error.
  • Configurable Sampling: Control the volume of observation events to optimize performance.
  • Instance Registry: Get runtime insights into active and disposed instances.

TelemetryAdapter:

  • LGTM Stack Integration: Out-of-the-box support for sending telemetry data to Loki (logs) and Mimir (metrics).
  • Event Buffering & Flushing: Efficiently collect and send events in batches, with configurable buffer size and flush intervals.
  • Automatic Retries: Robust error handling for failed telemetry uploads with exponential backoff.
  • Comprehensive Metrics: Tracks core metrics like total instances, method call counts, method durations (histograms), and error rates.
  • Authentication Support: Securely send data with basic authentication.
  • Dynamic Control: Enable and disable telemetry collection at runtime.

Installation

Already installed as part of @asaidimu/utils. Just import:

// Due to temporary stubbing in src/class/index.ts, direct imports from here might not work.
// For development, consider importing from the specific files, e.g.:
import { createClass, type ClassEvent } from '@asaidimu/utils/class/factory';
import { TelemetryRegistry, type TelemetryConfig } from '@asaidimu/utils/class/observer';

// Once fixed, you would import directly:
// import { createClass, TelemetryRegistry, type ClassEvent, type TelemetryConfig } from '@asaidimu/utils/class';

Usage Examples

Basic Class Creation with createClass

The initializer function returns an object defining instance properties and methods.

import { createClass } from '@asaidimu/utils/class/factory'; // Adjust import if stub is removed

interface GreeterInstance { message: string; greet(): string; }
const GreeterClass = createClass<GreeterInstance, { initialMessage: string }>(
  (params) => ({
    message: params.initialMessage,
    greet() { return `Hello, ${this.message}!`; }
  })
);
const greeter = new GreeterClass({ initialMessage: 'World' });
console.log(greeter.greet()); // Output: Hello, World!
Class with Cleanup Logic

Return a tuple [instance, cleanupFunction] for automatic cleanup when the instance is garbage collected.

import { createClass } from '@asaidimu/utils/class/factory'; // Adjust import if stub is removed

interface ResourceHolder { id: string; }
const ResourceClass = createClass<ResourceHolder, { name: string }>(
  (params) => {
    const resourceId = `resource-${params.name}-${Date.now()}`;
    console.log(`Resource ${resourceId} acquired.`);
    const instance: ResourceHolder = { id: resourceId };
    const cleanup = () => {
      console.log(`Resource ${resourceId} automatically released (GC).`);
    };
    return [instance, cleanup];
  }
);
let res = new ResourceClass({ name: 'db-connection' });
// Cleanup will be triggered when `res` is garbage collected (non-deterministic).
Singleton Management

Use singleton: true for a global singleton, or keySelector for parameter-based singletons.

import { createClass } from '@asaidimu/utils/class/factory'; // Adjust import if stub is removed

// Global Singleton
const ConfigSingleton = createClass(() => ({ value: Math.random() }), { singleton: true });
const config1 = new ConfigSingleton();
const config2 = new ConfigSingleton();
console.log('Global singleton same instance:', config1 === config2);

// Parameter-Based Singleton
const CacheManager = createClass((params: { cacheKey: string }) => ({
  id: `cache-${params.cacheKey}`, createdAt: Date.now()
}), { keySelector: (params) => params.cacheKey });
const cacheA1 = new CacheManager({ cacheKey: 'user-data' });
const cacheA2 = new CacheManager({ cacheKey: 'user-data' });
const cacheB = new CacheManager({ cacheKey: 'product-data' });
console.log('Parameter-based singleton same instance:', cacheA1 === cacheA2);
console.log('Parameter-based singleton different instance:', cacheA1 === cacheB);
Telemetry Integration with TelemetryRegistry

Configure and initialize TelemetryRegistry (a singleton instance of TelemetryAdapter) and pass its observe method to your classes.

import { createClass } from '@asaidimu/utils/class/factory'; // Adjust import if stub is removed
import { TelemetryRegistry, type TelemetryConfig } from '@asaidimu/utils/class/observer'; // Adjust import if stub is removed

const telemetryConfig: TelemetryConfig = {
  lgtm: { lokiUrl: 'http://localhost:3100/loki/api/v1/push', tempoUrl: 'http://localhost:3200/api/traces', mimirUrl: 'http://localhost:9009/api/v1/push', },
  metadata: { serviceName: 'my-app-telemetry-demo', environment: 'staging' },
  bufferConfig: { size: 10, flushInterval: 1000 }, enabledByDefault: true,
};
const telemetry = new TelemetryRegistry(telemetryConfig); // Creates/retrieves the singleton

interface MyServiceInstance { processData(data: string): string; generateError(): void; }
const MyService = createClass<MyServiceInstance, { id: number }>(
  (params) => ({
    processData(data: string) { console.log(`Processing data for service ${params.id}: ${data}`); return `Processed: ${data.toUpperCase()}`; },
    generateError() { throw new Error(`Error from MyService ${params.id}`); }
  }),
  { observer: telemetry.observe('MyService'), sampling: { global: 1 } }
);
const service1 = new MyService({ id: 1 });
service1.processData('hello world');
try { service1.generateError(); } catch (e) { console.error('Caught expected error from service1.'); }

// Events are buffered and sent periodically to Loki and Mimir.
// Force flush and stop intervals on app shutdown:
(async () => {
  await telemetry.cleanup();
  console.log('Telemetry cleaned up.');
})();

@asaidimu/utils/cache

@asaidimu/utils/cache provides an intelligent, configurable in-memory cache solution designed for optimal performance, data consistency, and developer observability. It implements common caching patterns like stale-while-revalidate and Least Recently Used (LRU) eviction, along with advanced features like automatic retries for failed fetches, extensible persistence mechanisms, and a comprehensive event system for real-time monitoring.

Key Features

  • Configurable In-Memory Store: Fast access to cached data.
  • Stale-While-Revalidate (SWR): Serve stale data immediately while fetching new data in the background, minimizing perceived latency.
  • Automatic Retries: Configurable retry attempts and exponential backoff for fetchFunction failures.
  • Pluggable Persistence: Integrates with any SimplePersistence implementation (e.g., LocalStorage, IndexedDB from @asaidimu/utils/persistence) to save and restore cache state.
  • Configurable Eviction Policies: Time-Based (TTL) and Size-Based (LRU).
  • Comprehensive Event System: Subscribe to granular cache events (hit, miss, fetch, error, eviction, invalidation, persistence, set_data) for logging, debugging, and advanced reactivity.
  • Performance Metrics: Built-in tracking for hits, misses, fetches, errors, evictions, and stale hits, with calculated hit rates.
  • Flexible Query Management: Register fetchFunctions for specific keys.
  • Imperative Control: Methods for manual invalidate, prefetch, refresh, setData, and remove operations.
  • TypeScript Support: Fully typed API.

Installation

Already installed as part of @asaidimu/utils. Just import:

import { Cache, type CacheOptions, type CacheEvent, type CacheEventType } from '@asaidimu/utils/cache';
import { type SimplePersistence } from '@asaidimu/utils/persistence'; // For persistence integration

Usage Examples

Basic Cache Initialization & Query Registration
import { Cache } from '@asaidimu/utils/cache';

const myCache = new Cache({ staleTime: 5000, cacheTime: 60000 }); // 5s stale, 1m cache

myCache.registerQuery('user/123', async () => {
  console.log('Fetching user data...');
  await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate network delay
  return { id: 123, name: 'Alice', email: 'alice@example.com' };
});

async function getUserData() {
  const userData = await myCache.get('user/123');
  console.log('User data:', userData);
}

getUserData(); // First call: Triggers fetch
setTimeout(() => getUserData(), 500); // Subsequent calls (within staleTime): Data returned instantly
setTimeout(() => getUserData(), 6000); // Call after staleTime: Data returned instantly, background fetch triggered
Pluggable Persistence Integration

@asaidimu/utils/cache can use any SimplePersistence implementation.

import { Cache } from '@asaidimu/utils/cache';
import { WebStoragePersistence } from '@asaidimu/utils/persistence';
import { v4 as uuidv4 } from 'uuid'; // For persistenceId

const webPersistence = new WebStoragePersistence<any>('my-app-cache-store');
const persistedCache = new Cache({
  staleTime: 5 * 60 * 1000, cacheTime: 30 * 60 * 1000,
  persistence: webPersistence, // Plug in the persistence adapter
  persistenceId: uuidv4(), // Unique ID for this cache instance in storage
  persistenceDebounceTime: 750, // Debounce saves to storage
});

persistedCache.registerQuery('config/features', async () => {
  console.log('Fetching feature flags...');
  return { darkTheme: true, notifications: false };
});

async function loadAndUseConfig() {
  const config = await persistedCache.get('config/features');
  console.log('Loaded config:', config);
}
loadAndUseConfig();
Real-time Monitoring with Events

Subscribe to various CacheEvent types for insights into cache behavior.

import { Cache } from '@asaidimu/utils/cache';

const monitorCache = new Cache({ enableMetrics: true });
monitorCache.registerQuery('weather/london', async () => {
  const temp = (Math.random() * 15 + 10).toFixed(1);
  console.log(`Fetching London temp: ${temp}°C`);
  return { city: 'London', temperature: parseFloat(temp), unit: 'C' };
}, { staleTime: 5000 }); // Very short staleTime for frequent fetches

monitorCache.on('fetch', (e) => { console.log(`[EVENT] Fetching ${e.key} (attempt ${e.attempt})`); });
monitorCache.on('hit', (e) => { console.log(`[EVENT] Cache hit for ${e.key}. Stale: ${e.isStale}`); });
monitorCache.on('error', (e) => { console.error(`[EVENT] Cache ERROR for ${e.key}:`, e.error.message); });

setInterval(() => { monitorCache.get('weather/london'); }, 1000);
setInterval(() => {
  const stats = monitorCache.getStats();
  console.log(`\n--- CACHE STATS ---\nSize: ${stats.size}, Hits: ${stats.metrics.hits}, Misses: ${stats.metrics.misses}\n---`);
}, 10000);

@asaidimu/utils/store

@asaidimu/utils/store is a comprehensive, type-safe, and reactive state management library for TypeScript applications, featuring robust middleware, transactional updates, deep observability, and an optional persistence layer. It provides a highly performant and observable way to manage your application's data, ensuring type safety and predictability across complex state interactions.

Key Features

  • 📊 Type-safe State Management: Full TypeScript support for defining and interacting with your application state, leveraging DeepPartial<T> for precise, structural updates.
  • 🔄 Reactive Updates: Subscribe to granular changes at specific paths within your state or listen for any change, ensuring efficient re-renders or side effects.
  • 🧠 Composable Middleware System:
    • Transform Middleware: Modify, normalize, or enrich state updates before they are applied.
    • Blocking Middleware: Implement custom validation or authorization logic to prevent invalid state changes.
  • 📦 Atomic Transaction Support: Group multiple state updates into a single, atomic operation with rollback on failure, guaranteeing data integrity.
  • 💾 Optional Persistence Layer: Seamlessly integrate with any SimplePersistence<T> implementation to load and save state, ensuring data durability and synchronization across instances.
  • 🔍 Deep Observability & Debugging: An optional StoreObservability class provides unparalleled runtime introspection:
    • Event History: Detailed log of all internal store events.
    • State Snapshots: Configurable history of your state over time for inspection.
    • Time-Travel Debugging: Undo and redo state changes.
    • Performance Metrics: Real-time tracking of update count, listener executions, average update times.
    • Console Logging: Configurable, human-readable logging of store events.
  • Efficient Change Detection: Utilizes a custom diff algorithm to identify only the truly changed paths (string[]), optimizing listener notifications.
  • 🗑️ Property Deletion: Supports explicit property deletion within partial updates using Symbol.for("delete").
  • Concurrency Handling: Automatically queues and processes set updates to prevent race conditions.

Installation

Already installed as part of @asaidimu/utils. Just import:

import { ReactiveDataStore, StoreObservability, type DeepPartial, type StoreEvent } from '@asaidimu/utils/store';
import { SimplePersistence } from '@asaidimu/utils/persistence'; // For persistence integration

Usage Examples

Basic Usage

Creating a store, getting state, and setting updates.

import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils/store';

interface AppState { user: { name: string; isActive: boolean; }; products: Array<{ id: string; name: string; }>; }
const initialState: AppState = { user: { id: '123', name: 'Jane Doe', isActive: true }, products: [], };
const store = new ReactiveDataStore<AppState>(initialState);

console.log('Initial state:', store.get().user.name);

await store.set({ user: { name: 'Jane Smith', isActive: false } });
console.log('State after partial update:', store.get().user.name, store.get().user.isActive);

await store.set((state) => ({ products: [...state.products, { id: 'p3', name: 'Keyboard' }] }));
console.log('State after functional update:', store.get().products.length);

const unsubscribeUser = store.subscribe('user', (state) => { console.log('User data changed:', state.user.name); });
await store.set({ user: { name: 'New Jane' } }); // Triggers listener
unsubscribeUser(); // Stop listening
Middleware System

Middleware functions allow you to intercept and modify state updates, or block them.

import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils/store';

interface MyState { counter: number; logs: string[]; }
const store = new ReactiveDataStore<MyState>({ counter: 0, logs: [], });

// Transform Middleware: Logger and Timestamp
store.use({ name: 'LoggerMiddleware', action: (state, update) => {
    console.log('Middleware: Incoming update:', update);
    return { logs: [...state.logs, `Update processed at ${Date.now()}`] };
}});

// Blocking Middleware: Prevent negative counter
store.use({ block: true, name: 'NegativeCounterBlocker', action: (state, update) => {
    if (update.counter !== undefined && (state.counter + (update.counter as number)) < 0) {
      console.warn('Blocking update: Counter cannot go negative.');
      return false; // Block the update
    }
    return true; // Allow
}});

await store.set({ counter: 5 });
console.log('Counter after +5:', store.get().counter);
await store.set({ counter: -100 }); // This will be blocked
console.log('Counter after -100 attempt (should remain 5):', store.get().counter);
Transaction Support

Group multiple updates into a single atomic operation; if any fails, all roll back.

import { ReactiveDataStore } from '@asaidimu/utils/store';

interface BankAccount { balance: number; }
const accountA = new ReactiveDataStore<BankAccount>({ balance: 500 });
const accountB = new ReactiveDataStore<BankAccount>({ balance: 200 });

async function transferFunds(fromStore: ReactiveDataStore<BankAccount>, toStore: ReactiveDataStore<BankAccount>, amount: number) {
  await fromStore.transaction(async () => {
    if (fromStore.get().balance < amount) { throw new Error('Insufficient funds'); }
    await fromStore.set({ balance: fromStore.get().balance - amount });
    await toStore.set({ balance: toStore.get().balance + amount });
  });
}

// Successful transfer
try { await transferFunds(accountA, accountB, 100); } catch (e) { console.error(e); }
console.log('After success: A', accountA.get().balance, 'B', accountB.get().balance); // A: 400, B: 300

// Failed transfer (insufficient funds)
try { await transferFunds(accountA, accountB, 1000); } catch (e: any) { console.error('Transfer failed:', e.message); }
console.log('After fail: A', accountA.get().balance, 'B', accountB.get().balance); // A: 400, B: 300 (rolled back)
Store Observability

The StoreObservability class provides advanced debugging.

import { ReactiveDataStore, StoreObservability } from '@asaidimu/utils/store';

interface DebuggableState { count: number; }
const store = new ReactiveDataStore<DebuggableState>({ count: 0 });

const observability = new StoreObservability(store, {
  maxEvents: 100, maxStateHistory: 10, enableConsoleLogging: true, // See console output
  performanceThresholds: { updateTime: 50, middlewareTime: 20 },
});

await store.set({ count: 1 });
await store.set({ count: 2 });

console.log('\n--- Recent State Changes (Diff) ---');
const recentChanges = observability.getRecentChanges(2);
recentChanges.forEach((change, index) => console.log(`Change #${index}:`, change.changedPaths, change.to));

console.log('\n--- Time-Travel ---');
const timeTravel = observability.createTimeTravel();
await store.set({ count: 3 });
console.log('Current:', store.get().count);
if (timeTravel.canUndo()) { await timeTravel.undo(); console.log('After undo:', store.get().count); } // 2
if (timeTravel.canRedo()) { await timeTravel.redo(); console.log('After redo:', store.get().count); } // 3

// Disconnect observability when no longer needed
observability.disconnect();

🏗️ Project Architecture

@asaidimu/utils is structured as a Bun/Node.js monorepo, where each significant utility resides in its own sub-directory under src/ and is exposed as a named export via package.json's exports field.

Core Components

  • @asaidimu/utils/persistence: Provides the SimplePersistence interface and concrete implementations (WebStoragePersistence, IndexedDBPersistence, EphemeralPersistence) for various browser storage mechanisms.
  • @asaidimu/utils/class: Offers createClass for advanced class construction (singletons, lifecycle hooks, runtime observation via Proxy) and TelemetryAdapter for integrating observations with monitoring stacks (Loki/Mimir).
  • @asaidimu/utils/cache: Implements a robust in-memory cache with features like stale-while-revalidate, configurable eviction policies (TTL/LRU), automatic retries for fetches, and a pluggable persistence layer.
  • @asaidimu/utils/store: Provides a reactive data store (ReactiveDataStore) with type-safe state management, a composable middleware system, atomic transaction support, and deep observability features (StoreObservability).

Data Flow

The data flow is largely compartmentalized within each package, but there are notable inter-package dependencies:

  • @asaidimu/utils/cache depends on @asaidimu/utils/persistence for its pluggable persistence feature, allowing it to save and load its cache state to/from various storage backends.
  • @asaidimu/utils/store also depends on @asaidimu/utils/persistence for its optional persistence layer.
  • @asaidimu/utils/class uses its internal TelemetryAdapter to send formatted log and metric data to external observability systems (like Loki and Mimir).
  • All modules that rely on cross-tab communication (e.g., persistence adapters, store) utilize the external @asaidimu/events package for their internal event bus and BroadcastChannel-based mechanisms. IndexedDBPersistence also uses @asaidimu/indexed and @asaidimu/query.

Extension Points

  • SimplePersistence<T> Interface: This is a crucial extension point. By implementing this interface, you can integrate any custom storage backend (e.g., a remote API, service worker cache, Node.js file system) with both @asaidimu/utils/persistence adapters and the persistence features of @asaidimu/utils/cache and @asaidimu/utils/store.
  • Custom Middleware (for ReactiveDataStore): Users can inject their own Middleware and BlockingMiddleware functions using store.use() to customize update logic, add logging, validation, or side effects within the reactive store.
  • CacheOptions serializeValue/deserializeValue: Customize how cache entry data is converted to and from a serializable format within @asaidimu/utils/cache before interacting with the persistence layer.
  • Cache Event Listeners (on/off): The on/off methods in @asaidimu/utils/cache provide hooks for integrating cache behavior with your application's logging, analytics, UI reactivity, or debugging tools.
  • createClass observer Callback: Provide a custom observer function to createClass in @asaidimu/utils/class to tap into instance lifecycle, property access, and method call events, allowing for bespoke monitoring or debugging integrations.

🤝 Development & Contributing

We welcome contributions to @asaidimu/utils! Please follow these guidelines to ensure a smooth collaboration.

Development Setup

To set up the project for local development:

  1. Clone the repository:
    git clone https://github.com/asaidimu/erp-utils.git
    cd erp-utils
  2. Install dependencies:
    bun install # or npm install or yarn install
    This will install all necessary development dependencies, including TypeScript, ESLint, Vitest, and build tools.

Scripts

The following bun (or npm/yarn) scripts are available at the monorepo root:

  • bun ci: Installs dependencies (equivalent to bun install).
  • bun clean: Removes the dist directory.
  • bun prebuild: Cleans the dist directory and runs scripts/prebuild.ts to prepare dist.package.json.
  • bun build: Compiles the TypeScript source files to JavaScript for all packages using tsup.
  • bun postbuild: Runs scripts/postbuild.ts after the build to copy READMEs and package.json.
  • bun test: Runs the test suite for all packages using Vitest.
  • bun test:browser: Runs browser tests using Vitest (requires Playwright setup).
  • bun lint: Lints the codebase using ESLint.
  • bun format: Formats the code using Prettier (usually handled by eslint).

Testing

Tests are written using Vitest.

  • To run all tests:
    bun test
  • To run tests in watch mode during development:
    bun test -- --watch
  • Ensure that your changes are covered by new or existing tests, and that all tests pass.

Contributing Guidelines

  • Fork the repository and create your branch from main.
  • Follow existing coding standards: Adhere to the TypeScript, ESLint, and Prettier configurations.
  • Commit messages: Use Conventional Commits for clear and consistent commit history (e.g., feat(persistence): add IndexedDB adapter, fix(cache): resolve eviction bug).
  • Pull Requests:
    • Open a pull request against the main branch.
    • Provide a clear and detailed description of your changes.
    • Reference any related issues.
    • Ensure all tests pass and the code is lint-free.
  • Issues: Report bugs or suggest features on the GitHub Issue Tracker.

Issue Reporting

If you find a bug or have a feature request, please open an issue on our GitHub Issues page. When reporting a bug, please include:

  • A clear, concise title.
  • Steps to reproduce the issue.
  • Expected behavior.
  • Actual behavior.
  • Your environment (browser, Node.js/Bun version, relevant @asaidimu/utils sub-package version).

📚 Additional Information

Troubleshooting

  • Browser Storage Limits: localStorage and sessionStorage have size limitations (typically 5-10 MB). For larger data sets, use IndexedDBPersistence from @asaidimu/utils/persistence.
  • JSON Parsing Errors: Ensure that data persisted via WebStoragePersistence or IndexedDBPersistence is JSON-serializable. Use serializeValue/deserializeValue options in @asaidimu/utils/cache for complex types.
  • Cross-Origin Restrictions: Browser storage is typically restricted to the same origin (protocol, host, port).
  • async Operations: Remember that IndexedDBPersistence methods (set, get, clear) and Cache methods like get, refresh, clear return Promises. Always use await or .then().
  • instanceId Usage: The instanceId parameter in SimplePersistence.set and SimplePersistence.subscribe is crucial for cross-tab synchronization. Ensure each tab/instance generates a unique ID (e.g., a UUID) upon startup and consistently uses it.
  • createClass Stubbed: Note that createClass and TelemetryRegistry exports from @asaidimu/utils/class are currently stubbed in src/class/index.ts. For full functionality, you may need to import them directly from factory.ts and observer.ts within the package's src/ directory. This is a temporary measure indicated by the codebase.
  • Telemetry Not Sending:
    • Verify your lokiUrl and mimirUrl in TelemetryConfig.
    • Check network connectivity and browser console/Node.js output for fetch errors.
    • Ensure TelemetryAdapter is enabled.
    • Check sampling configuration if events are not appearing.
  • Cache Cleanup Not Triggering: FinalizationRegistry (used by createClass) and time-based garbage collection (in Cache) are non-deterministic. For immediate resource release, implement explicit dispose() or destroy() methods on your instances.
  • ReactiveDataStore Update Issues:
    • If subscribe callbacks aren't firing, ensure you're subscribing to the correct path, or that the value is strictly different.
    • If transaction rollbacks don't work, ensure errors are thrown within the transaction callback and promises are awaited.
    • If middleware isn't applied, ensure it's registered before the update and its return type is correct.

FAQ

Q: What is the LGTM Stack? A: LGTM stands for Loki, Grafana, Tempo, Mimir. It's a suite of open-source tools from Grafana Labs for observability: * Loki: A log aggregation system for collecting logs. * Grafana: A visualization and dashboarding tool. * Tempo: A distributed tracing backend. * Mimir: A scalable, long-term storage for Prometheus metrics. @asaidimu/utils/class's TelemetryAdapter specifically integrates with Loki (for logs/events) and Mimir (for metrics).

Q: How does staleTime differ from cacheTime in @asaidimu/utils/cache? A: staleTime determines when data is considered "stale" and a background refetch should be triggered (you can still use the stale data). cacheTime determines how long an item can remain unaccessed before it's eligible for garbage collection and removal from the cache. An item can be stale but still within its cacheTime.

Q: Can I use these utilities in a Node.js environment? A: @asaidimu/utils/class and parts of @asaidimu/utils/store can be used in both Node.js and browser environments. However, @asaidimu/utils/persistence and @asaidimu/utils/cache (when using browser-specific persistence adapters) are primarily designed for browser environments as they rely on browser-specific APIs like localStorage, sessionStorage, IndexedDB, and BroadcastChannel. You can implement custom SimplePersistence for Node.js (e.g., file system or database) to use @asaidimu/utils/cache or @asaidimu/utils/store server-side.

Q: What is Symbol.for("delete") in @asaidimu/utils/store? A: It's a special JavaScript Symbol used to explicitly remove a property from the state during a ReactiveDataStore.set() operation. If you pass Symbol.for("delete") as the value for a key in your DeepPartial update, that key will be removed from the state.

Q: What is the performance impact of createClass's observation features? A: createClass uses JavaScript Proxy objects for observation, which have a slight overhead. However, this is generally negligible for most application use cases. The sampling option allows you to control the volume of events, significantly reducing the overhead of event processing and transmission if fine-grained monitoring isn't always required. TelemetryAdapter buffers events and flushes them asynchronously to minimize blocking the main thread.

Changelog / Roadmap

  • Changelog: For detailed changes between versions, please refer to the CHANGELOG.md file at the root of the repository.
  • Roadmap: Future plans include:
    • Adding more specialized utility packages.
    • Further optimization and performance enhancements.
    • Expanding telemetry integration options.
    • Resolving the temporary stubbing of createClass and TelemetryRegistry in @asaidimu/utils/class's main export.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

Acknowledgments

This library leverages and builds upon the excellent work from:

  • @asaidimu/events: For robust cross-tab event communication.
  • @asaidimu/indexed: For simplified IndexedDB interactions.
  • @asaidimu/query: For a declarative query builder used with IndexedDB.
  • uuid: For generating unique identifiers.
  • Inspired by various dependency injection, state management, and observability patterns in modern software architecture (e.g., React Query, SWR).
  • Built with TypeScript and tested with Vitest.

Readme

Keywords

Package Sidebar

Install

npm i @asaidimu/utils

Weekly Downloads

0

Version

2.2.0

License

MIT

Unpacked Size

374 kB

Total Files

24

Last publish

Collaborators

  • asaidimu