@asaidimu/utils-class
v2.0.2
Published
Class management utilities
Readme
@asaidimu/utils-class
Advanced Class Management & Observability for TypeScript
A powerful TypeScript library providing advanced class factory patterns with built-in lifecycle observation, robust singleton management, and integrated telemetry for comprehensive application monitoring.
Quick Links
- Overview & Features
- Installation & Setup
- Usage Documentation
- Project Architecture
- Development & Contributing
- Additional Information
Overview & Features
@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 manage, a highly configurable factory function that goes beyond standard class constructors. It introduces advanced patterns like robust singleton management (global or parameter-based), automatic instance cleanup, and a powerful observation mechanism to monitor object creation, property access, and method calls.
Complementing manage, the library provides TelemetryAdapter, a ready-to-use solution for integrating these observations with your observability stack. It collects, buffers, and sends structured logs (to Loki) and metrics (to Mimir), giving you real-time insights into your application's performance and health. This combination empowers developers to build more maintainable, scalable, and operationally transparent systems.
🚨 IMPORTANT NOTE: Current Version Status 🚨
Please be aware that the currently published version of @asaidimu/utils-class (version 1.0.0) contains a stubbed implementation for the manage function. This means the core functionality described below (instance management, singleton patterns, observation, and telemetry) is not yet active or fully functional in the index.js or index.mjs entry points. The detailed features and usage examples provided in this README describe the intended design and future capabilities of the library as it progresses towards a stable release.
We are actively working on fixing the underlying code and passing all tests to enable these features. Thank you for your understanding and patience!
Key Features (Intended/Planned)
🚀 manage:
- Flexible Class Instantiation: Create classes from plain object initializers (sync or async).
- Automated Cleanup: Register cleanup callbacks for instances, integrating with JavaScript's
FinalizationRegistryfor automatic resource management. - Robust Singleton Patterns: Implement global singletons or parameter-based singletons using
keySelectorfor dynamic instance caching. - Lifecycle & Usage Observation: Monitor critical events including:
create: Instance creation.cleanup: Instance garbage collection and cleanup execution.access: Property read access.method-call: Method execution (sync/async, success/failure, duration).error: Errors during creation, cleanup, or method calls.
- 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.
- Data Sanitization: Automatically cleans up sensitive or excessively large data fields before sending.
Installation & Setup
Prerequisites
- Node.js: Version 18 or higher (LTS recommended)
- npm or Yarn or Bun: A package manager for Node.js
Installation Steps
Install the library using your preferred package manager:
bun add @asaidimu/utils-class
# or
npm install @asaidimu/utils-class
# or
yarn add @asaidimu/utils-classConfiguration (for Telemetry)
To utilize the telemetry features (once they are active in a future release), you'll need to configure the TelemetryRegistry with your LGTM stack endpoints and application metadata.
import { TelemetryRegistry, type TelemetryConfig } from '@asaidimu/utils-class';
const telemetryConfig: TelemetryConfig = {
lgtm: {
lokiUrl: 'http://localhost:3100/loki/api/v1/push', // Your Loki endpoint
tempoUrl: 'http://localhost:3200/api/traces', // Not directly used by TelemetryAdapter currently, but part of LGTM config
mimirUrl: 'http://localhost:9009/api/v1/push', // Your Mimir endpoint
auth: { // Optional: if your LGTM stack requires basic auth
username: 'your-username',
password: 'your-password'
}
},
metadata: {
serviceName: 'my-awesome-app',
environment: 'development',
version: '1.0.0',
labels: { // Optional additional labels for all telemetry
team: 'frontend',
region: 'us-east-1'
}
},
bufferConfig: {
size: 50, // Buffer up to 50 events before flushing
flushInterval: 2000, // Flush every 2 seconds if buffer is not full
maxRetries: 5, // Max retries for failed requests
retryDelay: 500, // Initial retry delay in ms
maxBufferAge: 60000 // Drop events older than 60 seconds from buffer
},
enabledByDefault: true, // Optional: default to true
metrics: [ // Optional: Define custom metrics
// { name: 'my_custom_counter', type: 'counter', description: 'A custom counter', labels: ['status'] }
]
};
// Initialize the global TelemetryRegistry (it's a singleton)
const telemetry = new TelemetryRegistry(telemetryConfig);
// You can observe directly or pass the adapter.observe method to manage
// const observer = telemetry.observe('MyClass');
// To disable/enable telemetry dynamically:
// telemetry.disable();
// telemetry.enable();Verification
After installation, you can verify it by importing the library. Please note that the primary manage function is currently a stub.
import { manage, TelemetryRegistry } from '@asaidimu/utils-class';
console.log('Library imported successfully!');
// The manage function currently logs a message indicating it's a stub.
const MyClass = manage(() => ({ value: 1 }));
const instance = new MyClass(); // This will log "THIS METHOD IS A STUB UNTIL WE FIX THE CODE AND TESTS PASS"
// The 'instance' will be an empty object or whatever the stub returns.Usage Documentation
⚠ Note: The examples below demonstrate the intended usage of manage and TelemetryRegistry once the stubbed functionality is fully implemented. In the current 1.0.0 version, these examples will not produce the described behavior due to the manage stub.
manage Fundamentals
The manage function is the primary entry point for creating managed classes. It takes an initializer function and an optional options object.
Basic Class Creation
The initializer function should return an object that defines the instance's properties and methods.
import { manage } from '@asaidimu/utils-class';
// 1. Synchronous initializer
interface GreeterInstance {
message: string;
greet(): string;
}
const GreeterClass = manage<GreeterInstance, { initialMessage: string }>(
(params) => ({
message: params.initialMessage,
greet() { return `Hello, ${this.message}!`; }
})
);
const greeter = new GreeterClass({ initialMessage: 'World' });
console.log(greeter.greet()); // Expected Output: Hello, World!
// 2. Async Initializer (returns a Promise)
interface AsyncDataLoader {
data: string;
fetchData(): Promise<string>;
}
const AsyncClass = manage<AsyncDataLoader, { id: number }>(
async (params) => {
const fetchedData = await new Promise<string>(resolve => setTimeout(() => resolve(`Data for ${params.id}`), 100));
return {
data: fetchedData,
async fetchData() { return this.data; }
};
}
);
(async () => {
const instancePromise = new AsyncClass({ id: 123 });
console.log(instancePromise instanceof Promise); // Expected Output: true
const instance = await instancePromise;
console.log(instance.data); // Expected Output: Data for 123
})();Class with Cleanup Logic
Your initializer can optionally return a tuple [instance, cleanupFunction] to define a cleanup callback that will be invoked when the instance is garbage collected.
import { manage } from '@asaidimu/utils-class';
interface ResourceHolder {
id: string;
release(): void;
}
const ResourceClass = manage<ResourceHolder, { name: string }>(
(params) => {
const resourceId = `resource-${params.name}-${Date.now()}`;
console.log(`Resource ${resourceId} acquired.`);
const instance: ResourceHolder = {
id: resourceId,
release: () => console.log(`Resource ${resourceId} explicitly released.`)
};
const cleanup = () => {
console.log(`Resource ${resourceId} automatically released (GC).`);
// Perform actual resource cleanup here (e.g., close database connections, clear timers)
};
return [instance, cleanup];
}
);
(async () => {
let res = new ResourceClass({ name: 'db-connection' });
// The 'Resource acquired' message will be logged immediately.
// If `res` goes out of scope, the cleanup function will eventually be called by GC.
// For immediate cleanup, you'd typically have a `dispose` method on the instance itself.
res.release(); // Explicit release
// To trigger GC for testing (not recommended in production, non-deterministic):
// res = null as any;
// global.gc && global.gc(); // Requires Node.js --expose-gc flag
})();Factory Function Mode
Instead of a class constructor, manage can return a direct factory function by setting factory: true in options.
import { manage } from '@asaidimu/utils-class';
interface ServiceInstance {
id: string;
}
const createService = manage<ServiceInstance, { name: string }>(
(params) => ({ id: `service-${params.name}` }),
{ factory: true } // Returns a function instead of a class
);
const myService1 = createService({ name: 'auth' });
const myService2 = createService({ name: 'logger' });
console.log(myService1.id); // Expected Output: service-auth
console.log(myService2.id); // Expected Output: service-loggerSingleton Management
manage supports two modes of singleton management: global and parameter-based.
Global Singleton
Returns the exact same instance on every call.
import { manage } from '@asaidimu/utils-class';
interface GlobalConfig {
value: number;
}
const ConfigSingleton = manage<GlobalConfig, {}>(
() => ({ value: Math.random() }),
{ singleton: true } // Enable global singleton
);
const config1 = new ConfigSingleton();
const config2 = new ConfigSingleton();
console.log(config1 === config2); // Expected Output: true
console.log(config1.value === config2.value); // Expected Output: trueParameter-Based Singleton
Creates a new instance for each unique set of constructor parameters (or derived key).
import { manage } from '@asaidimu/utils-class';
interface CacheInstance {
id: string;
createdAt: number;
}
const CacheManager = manage<CacheInstance, { cacheKey: string }>(
(params) => ({
id: `cache-${params.cacheKey}`,
createdAt: Date.now()
}),
{
keySelector: (params) => params.cacheKey // Use `cacheKey` parameter as the singleton key
}
);
const cacheA1 = new CacheManager({ cacheKey: 'user-data' });
const cacheA2 = new CacheManager({ cacheKey: 'user-data' }); // Returns existing instance
const cacheB = new CacheManager({ cacheKey: 'product-data' }); // Creates new instance
console.log(cacheA1 === cacheA2); // Expected Output: true
console.log(cacheA1 === cacheB); // Expected Output: false
console.log(cacheA1.createdAt === cacheA2.createdAt); // Expected Output: true
console.log(cacheA1.createdAt !== cacheB.createdAt); // Expected Output: trueLifecycle Observation
The observer option allows you to receive detailed events about class instances. This is crucial for monitoring and debugging.
import { manage, type InstanceEvent } from '@asaidimu/utils-class';
// Define a custom observer function
const myCustomObserver = (event: InstanceEvent<any, any>) => {
console.log(`[${event.type.toUpperCase()}] Instance: ${event.instanceId}`);
if (event.property) console.log(` Property: ${String(event.property)}`);
if (event.time !== undefined) console.log(` Duration: ${event.time.toFixed(2)}ms`);
if (event.error) console.error(` Error: ${event.error.message}`);
console.log('---');
};
interface MyObservableClassInstance {
value: number;
increment(amount: number): number;
asyncFetch(): Promise<string>;
failingMethod(): void;
}
const MyObservableClass = manage<MyObservableClassInstance, { initialValue: number }>(
(params) => ({
value: params.initialValue,
increment(amount: number) {
this.value += amount;
return this.value;
},
async asyncFetch() {
await new Promise(resolve => setTimeout(resolve, 50));
return 'data fetched';
},
failingMethod() {
throw new Error('This method always fails!');
}
}),
{
observer: myCustomObserver, // Pass your observer function
sampling: { global: 1 } // Ensure all events are reported (default is 1)
}
);
const instance = new MyObservableClass({ initialValue: 100 });
// Expected Output: [CREATE] Instance: inst_... (initialization event)
console.log('Initial value:', instance.value); // Property access triggers event
// Expected Output: [ACCESS] Instance: inst_... Property: value
instance.increment(10); // Method call triggers event
// Expected Output: [METHOD-CALL] Instance: inst_... Property: increment ...
(async () => {
await instance.asyncFetch(); // Async method call triggers event
// Expected Output: [METHOD-CALL] Instance: inst_... Property: asyncFetch ... async: true ...
try {
instance.failingMethod(); // Failing method call triggers error event
} catch (e) {
console.error('Caught expected error.');
// Expected Output: [METHOD-CALL] Instance: inst_... Property: failingMethod ... success: false ... Error: This method always fails!
}
})();
// When 'instance' is eventually garbage collected, a 'CLEANUP' event will be reported.Sampling Configuration
Reduce overhead by sampling observation events:
import { manage } from '@asaidimu/utils-class';
const observer = (event: any) => console.log(`Observed: ${event.type}`);
const SampledClass = manage(() => ({
doSomething() {}
}), {
observer: observer,
sampling: {
global: 0.1, // Only 10% of all events will be reported
eventTypes: {
'method-call': 0.5, // 50% of method calls
'create': 1 // 100% of creation events (overrides global for this type)
}
}
});
const instance = new SampledClass();
instance.doSomething();
instance.doSomething();
// Events will be reported based on the configured probabilities.Telemetry Integration (LGTM Stack)
The TelemetryAdapter is a pre-built observer that formats and sends events to Loki (logs) and Mimir (metrics). The TelemetryRegistry is a singleton instance of TelemetryAdapter for easy access.
import { manage, TelemetryRegistry, type TelemetryConfig } from '@asaidimu/utils-class';
// 1. Configure and initialize TelemetryRegistry
const telemetryConfig: TelemetryConfig = {
lgtm: {
lokiUrl: 'http://localhost:3100/loki/api/v1/push',
tempoUrl: 'http://localhost:3200/api/traces', // Not used by observer.ts currently
mimirUrl: 'http://localhost:9009/api/v1/push',
},
metadata: {
serviceName: 'my-app-telemetry-demo',
environment: 'staging'
},
bufferConfig: { size: 10, flushInterval: 1000 }
};
const telemetry = new TelemetryRegistry(telemetryConfig); // This creates or retrieves the singleton instance
// 2. Create a class and link it to the telemetry observer
interface MyServiceInstance {
processData(data: string): string;
generateError(): void;
}
const MyService = manage<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'), // Pass the specific observer for 'MyService' class
sampling: { global: 1 } // Ensure all events are reported for this example
}
);
// 3. Use your class
const service1 = new MyService({ id: 1 });
service1.processData('hello world');
const service2 = new MyService({ id: 2 });
service2.processData('another message');
try {
service1.generateError();
} catch (e) {
console.error('Caught expected error from service1.');
}
// Events are buffered and sent periodically to Loki and Mimir.
// You can force a flush for testing/shutdown:
(async () => {
await telemetry.cleanup(); // Flushes remaining events and stops intervals
console.log('Telemetry cleaned up.');
})();Expected telemetry data sent to Loki (logs):
- Log entries for
create,access,method-call,errorevents, including instance details, class tag, and operation timings.
Expected telemetry data sent to Mimir (metrics):
class_instances_total{class="MyService",service="my-app-telemetry-demo",environment="staging"}(gauge)class_method_calls_total{class="MyService",method="processData",success="true",async="false",service="..."}(counter)class_method_duration_seconds_bucket{class="MyService",method="processData",le="..."}(histogram buckets)class_errors_total{class="MyService",error_type="Error",phase="usage",service="..."}(counter)
Instance Registry
Both manage and the factory function it can return provide a static getInstanceRegistry() method to inspect currently tracked instances.
import { manage } from '@asaidimu/utils-class';
const MyTrackedClass = manage(() => ({
id: Math.random(),
}));
const instance1 = new MyTrackedClass();
const instance2 = new MyTrackedClass();
console.log('Instance Registry:', MyTrackedClass.getInstanceRegistry());
/* Expected Output example:
{
activeCount: 2,
totalTracked: 2,
instances: [
{
id: 'inst_1_...',
createdAt: <Date>,
lastActiveAt: <Date>,
age: <number>ms,
isDisposed: false,
hasCleanup: false
},
// ... for instance 2
]
}
*/
// If you returned a factory function:
const createMyTrackedService = manage(() => ({ name: 'test' }), { factory: true });
const service = createMyTrackedService();
console.log('Service Factory Registry:', createMyTrackedService.getInstanceRegistry());Project Architecture
The @asaidimu/utils-class library is structured around two core modules: factory and observer.
src/class/
├── factory.ts # Core logic for manage, instance management, and observation proxying
├── observer.ts # TelemetryAdapter for LGTM integration, event buffering, and metrics
├── index.ts # Re-exports from factory and observer for easy import (currently contains stub)
├── factory.test.ts # Vitest tests for manage
├── observer.test.ts # Vitest tests for TelemetryAdapter
└── README.md # This documentCore Components
manage<T, P>(initializer, options)(exposed fromindex.ts, implemented infactory.ts): The primary factory function. It takes aninitializer(a function returning an instance object or[instance, cleanup]) andoptionsto configure behavior like singleton mode, key-based caching, and event observation. It leveragesProxyfor method and property access interception, andFinalizationRegistry/WeakReffor garbage collection-aware cleanup and instance tracking.InstanceEvent<T, P>(infactory.ts): A TypeScript type definition for the structured events generated bymanage's observation mechanism. These events capture details about creation, cleanup, errors, property accesses, and method calls.TelemetryAdapter<T, P>(config)(inobserver.ts): A class that acts as anobserverforInstanceEvents. It buffers these events and transforms them into appropriate formats for Loki (logs) and Mimir (metrics), then sends them viafetch. It manages buffering, retries, and authentication.TelemetryRegistry(inobserver.ts): A singleton instance ofTelemetryAdapter, created usingmanageitself, ensuring a single, globally accessible telemetry client.
Data Flow (Intended)
- Instance Creation/Usage: When a class generated by
manageis instantiated, or its methods/properties are accessed,InstanceEventobjects are generated internally. - Observation Callback: These
InstanceEvents are passed to theobservercallback provided inmanageoptions. - Telemetry Adapter: If
TelemetryAdapter.observe()is used as the observer, it receives these events. - Buffering & Metrics: The
TelemetryAdapterbuffers the events and updates its internal metric state. - Flushing: Periodically (or when the buffer is full),
TelemetryAdapterformats the buffered events into Loki-compatible log streams and Mimir-compatible Prometheus text format metrics. - Transmission: The formatted data is sent to the configured Loki and Mimir endpoints using
fetch. - LGTM Stack: Loki stores the logs, and Mimir ingests the metrics, making them available for querying and visualization (e.g., in Grafana).
Development & Contributing
Development Setup
To set up the project for local development:
- Clone the repository:
git clone https://github.com/asaidimu/erp-utils.git # This module is part of a monorepo cd erp-utils/src/class # Navigate to this specific module - Install dependencies:
npm install # or yarn install # or bun install - Build the project:
npm run build # or yarn build # or bun run build
Scripts
npm test: Runs all unit tests using Vitest.npm run build: Compiles TypeScript files to JavaScript.npm run lint: Runs ESLint for code quality checks.npm run format: Formats code using Prettier.
Testing
Tests are written using Vitest.
Note that some test suites (Observation, TelemetryAdapter) are currently skipped in the source code (.skip) and will not run by default until the underlying functionality is stable.
To run tests:
npm testTo run tests in watch mode:
npm test -- --watchTest coverage requirements are typically defined in the vitest.config.ts or package.json.
Contributing Guidelines
We welcome contributions! Please follow these steps:
- Fork the repository.
- Create a new branch for your feature or bug fix (
git checkout -b feature/my-new-feature). - Make your changes, ensuring they adhere to the existing coding style and standards.
- Write and run tests to cover your changes and ensure no regressions (
npm test). Pay special attention to the currently skipped tests if your changes relate to observation or telemetry. - Update documentation if your changes impact existing features or introduce new ones.
- Commit your changes using a descriptive commit message following Conventional Commits specifications (e.g.,
feat: add new observer option). - Push your branch to your forked repository.
- Open a Pull Request to the
mainbranch of the original repository.
Issue Reporting
If you find a bug or have a feature request, please open an issue on the GitHub Issues page. When reporting a bug, please include:
- A clear and concise description of the issue.
- Steps to reproduce the behavior.
- Expected vs. actual behavior.
- Any relevant error messages or console output.
- Your environment details (Node.js version, OS, etc.).
Additional Information
Troubleshooting
managedoes not work as expected:- As mentioned in the Overview, the
managefunction is currently a stub. It will not perform the described class management, singleton, or observation behaviors in the0.0.0version. This is the intended behavior until the stub is removed in a future release.
- As mentioned in the Overview, the
- Telemetry not sending (once enabled):
- Verify your
lokiUrlandmimirUrlinTelemetryConfig. - Check network connectivity to your LGTM stack.
- Ensure
fetchis available in your environment (Node.js versions below 18 might need a polyfill, though it's typically global). - Check browser console/Node.js output for
fetcherrors. - Confirm your
TelemetryAdapterisenabled.
- Verify your
- Events not observed (once enabled):
- Ensure
observeroption is correctly passed tomanage. - Check
samplingconfiguration; a very lowglobaloreventTyperate might prevent events from being reported. - Verify the observer function itself is logging or processing events.
- Ensure
- Cleanup not triggering:
FinalizationRegistryrelies on garbage collection, which is non-deterministic. Cleanup functions will eventually be called when the instance is no longer reachable, but there's no guarantee on when.- For immediate resource release, implement a
dispose()method on your instances.
FAQ
Q: Why is manage a stub?
A: The manage function in the current 0.0.0 version is a placeholder. This is part of the development process to ensure the core logic and tests are thoroughly validated before releasing the full functionality. We are actively working on it.
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: What is the performance impact of using manage and its observation features?
A: manage uses JavaScript Proxy objects for observation. While proxies have a slight performance overhead compared to direct object access, it's generally negligible for most application use cases. The sampling option for the observer 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.
Q: Can I use manage in a browser environment?
A: Yes, manage and TelemetryAdapter are designed to work in both Node.js and browser environments. The TelemetryAdapter intelligently uses window.setInterval/clearInterval in browsers and globalThis.setInterval/clearInterval in Node.js, and fetch is a standard Web API.
Changelog / Roadmap
- Changelog: See CHANGELOG.md for a history of changes.
- Roadmap: Future plans include:
- Enabling full
managefunctionality: Removing the stub and ensuring all tests pass. - Adding support for custom event properties in
TelemetryAdapter. - More advanced sampling strategies.
- Integration with other telemetry backends (e.g., OpenTelemetry).
- Enabling full
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Inspired by various dependency injection and observability patterns in modern software architecture.
- Built with TypeScript and tested with Vitest.
