Skip to content
This repository has been archived by the owner on Dec 13, 2021. It is now read-only.
/ common Public archive

[ARCHIVED] A collection of common utilities for my projects.

Notifications You must be signed in to change notification settings

bailey-archives/common

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚠️ This repository was originally created by @baileyherbert and is no longer maintained. It has been moved here for preservation. The code within it might be outdated or broken.

Common

This package contains several small classes, utilities, types, and polyfills that I use across most of my projects.

npm install @baileyherbert/common


Dependency Injection

Container

This is a dependency injection container that supports transient, singleton, and container-scoped instance resolution. It allows you to spawn child containers, as well as dispatchers for method invocation with DI.

Global container

Import the global container from anywhere:

import { container } from '@baileyherbert/common';

Registration

Then register your types using injection tokens using the register methods.

container.register(ClassType);
container.register(ClassType, { useClass: ClassType });
container.register(ClassType, { useValue: new ClassType() });
container.register(ClassType, { useFactory: () => new ClassType() });

container.registerSingleton(ClassType);
container.registerSingleton(ClassType, ClassType);

container.registerInstance(ClassType, new ClassType());

When registering a class or token provider, or a type, you can provide a lifecycle:

container.register(ClassType, { lifecycle: Lifecycle.Singleton });
container.register(ClassType, { useClass: ClassType }, { lifecycle: Lifecycle.ContainerScoped });
  • Transient creates a new instance for each resolution. This is the default.
  • Singleton creates a single instance and caches it for subsequent resolutions.
  • ContainerScoped creates a single instance per container (i.e. child containers will get their own).

Decorators

For the container to successfully resolve dependencies, all classes added to it must have the @Injectable decorator applied.

@Injectable()
export class ClassType {}

You can also register a class as a singleton on the global container using the @Singleton decorator. This will also mark the class as injectable so there's no need to add the @Injectable decorator.

@Singleton()
export class ClassType {}

You can also enable dependency injection on a class method by applying the @Injectable decorator to it.

@Singleton()
export class ClassType {
	@Injectable()
	public methodWithDI() {

	}
}

Resolution

To resolve a single instance, use the resolve method. The last provider to be registered will be used.

const instance = container.resolve(ClassType);

If multiple providers are registered, you can retrieve all of their instances as an array with the resolveAll method.

const instances = container.resolveAll(ClassType);

Child containers

You can create child containers on demand. By registering a dependency on a child container, you can override the return value of the resolve method. The resolveAll method will return an array of dependencies from both containers in the order of registration, and with the child container's dependencies last.

const child = container.createChildContainer();
child.registerInstance(ClassType, new ClassType());

Dispatchers

To invoke methods with dependency injection, first create a dispatcher.

const dispatcher = container.createDispatcher();

You can add custom typed instances which override the container. You can also add named values. If the method has a parameter which fails to resolve with the container or has a primitive type, but has a matching named value, then the named value will be used.

dispatcher.setNamedParameter('name', 'John Doe');
dispatcher.setTypedParameter(ClassType, new ClassType());

Finally, use the invoke method to resolve dependencies, execute, and get the return value.

const returnValue = dispatcher.invoke(object, 'methodName');

Context

Containers can store basic state information which is available to all of its users.

container.setContext('service', 'ServiceName');
container.setContext('id', 123);

Other parts of the application can retrieve the context.

const id = container.getContext<number>('id');
const service = container.getContext<string>('ServiceName');

resolver

This helper manages global container instances and makes it easy for various parts of the application to retrieve a reference to specific containers.

Named containers

If the global container is not sufficient, you can use named containers. Simply request a named container and it will be created and cached globally.

import { resolver } from '@baileyherbert/common';

const container = resolver.getInstance('name');

Container references

If your application is using multiple containers, you might be interested in storing a reference to the container used to construct an object. Generally, this would require injecting the container as a parameter.

The resolver instead makes the container available with the getConstructorInstance() method, but note that this method will throw an error if not called from within a constructor that has been invoked by the container during DI.

Here's a reliable pattern for storing the container that works even if the class is extended:

import { resolver } from '@baileyherbert/common';

export class DependencyInjectedClass {
	protected container = resolver.getConstructorInstance();

	public constructor() {
		// Now all methods, including the constructor, has a reference to the container
		this.container.resolve();
	}
}

With a reference to the container, you could make it easier for nested components in your application to retrieve top level objects, like a root App object.

export class DependencyInjectedClass {
	protected container = resolver.getConstructorInstance();
	protected app = this.container.resolve(App);
}

Events

EventEmitter

This is an alternative event emitter that works on all platforms. It allows you to specify the event types, and keeps the _emit method protected.

import { EventEmitter } from '@baileyherbert/common';

export class Chat extends EventEmitter<Events> {

	protected _onUserConnected(user: User) {
		this._emit('connected', user.username);
	}

}

type Events = {
	chat: [username: string, message: string];
	connected: [username: string];
	disconnected: [username: string];
};
const chat = new Chat();

chat.on('connected', username => {
	console.log('User %s connected.', username);
});

Logging

Logger

This class creates a logger for a named service or component. It accepts the same arguments as console.log(), and formats data the same way. Instead of sending output to stdout, this class emits log output via the log event.

import { Logger } from '@baileyherbert/common';

const logger = new Logger('app');

logger.on('log', event => {
	console.log(event.output);
});

logger.info('Hello world!');
logger.verbose('It works!');

Loggers can also spawn children for subcomponents. Their log output is forwarded back up to the root logger, so only a single listener is required.

const child = logger.createLogger('child');
child.info('This is from a child logger!');

LogConsoleWriter

This class can be used to print logger output to the console with colors, timestamps, and service names.

import { Logger, LogConsoleWriter, LogLevel } from '@baileyherbert/common';

// Create the logger
const logger = new Logger();

// Create the log console writer with verbosity set to 0 (verbose)
const writer = new LogConsoleWriter(LogLevel.Verbose);

// Mount the logger to the writer
// This immediately starts printing output to the console
writer.mount(logger);

LogFileWriter

This class can be used to forward logger output to a file. It can also rotate the log file automatically after it reaches a certain size. The default options are shown below.

import { LogFileWriter, LogLevel } from '@baileyherbert/common';

const writer = new LogFileWriter({
	fileName: 'console.log',
	logLevel: LogLevel.Info,
	encoding: 'utf8',
	formatOptions: {},
	formatEOL: '\n',             // Uses the system default
	logRotationSize: 52428800,   // 50 MiB
	logRotationDir: '.',         // Defaults to the dir of `fileName`
	logNameEnabled: true,
	logTimestampEnabled: true
});

writer.mount(logger);

Polyfills

Buffer

This class provides the same interface as Node's Buffer, but it works in browsers as well. When importing this class, it will always return Node's native implementation if available.

import { Buffer } from '@baileyherbert/common';

const buffer = Buffer.from('Hello world!', 'utf8');
const hex = buffer.toString('hex');

Promises

PromiseCompletionSource

This class allows you to create a Promise which can easily be resolved or rejected on demand from the outside.

import { PromiseCompletionSource } from '@baileyherbert/common';

function runFakeTask() {
	const source = new PromiseCompletionSource();

	// Resolves the promise after 5 seconds
	setTimeout(() => {
		source.setResult();
	}, 5000);

	// Returns the promise object
	return source.promise;
}

// Resolves after 5 seconds
await runFakeTask();

PromiseTimeoutSource

This class creates a promise that resolves to a boolean after the specified time, but can be cancelled prematurely. The boolean is true if the timeout was triggered, or false if cancelled.

You can also specify a custom action to execute when the timeout is reached.

import { PromiseTimeoutSource } from '@baileyherbert/common';

Example 1: Wait for 30 seconds

await new PromiseTimeoutSource(30000);

Example 2: Run a task after 30 seconds

new PromiseTimeoutSource(30000, () => {
	console.log('This runs after 30 seconds!');
});

Example 3: Cancel a task before it's scheduled to run

const timeout = new PromiseTimeoutSource(30000, () => {
	console.log('This runs after 30 seconds!');
});

// The action will never run because it gets cancelled after 15 sec!
setTimeout(() => timeout.cancel(), 15000);

// Confirm that it was cancelled
const result = await timeout;
console.log('The timeout was', result ? 'fulfilled' : 'cancelled');

You could use this to cancel and clean up an operation after a specified amount of time, but stop the cancellation task from running if it completes in time.


Reflection

ReflectionClass

This class is used to retrieve the methods and metadata of a class at runtime.

Reflection can see all methods on a class, but if you want to query its return or parameter types, you'll need to apply a decorator to it. If you don't have a decorator, use the built-in @Reflectable() decorator.

Here's an example class:

import { Reflectable } from '@baileyherbert/common';

class Test {
	@Reflectable()
	public printHello(name = 'world') {
		console.log('Hello', name);
	}
}

We'll query its own local (non-static) methods below and print their names and parameters.

import { ReflectionClass, MethodFilter } from '@baileyherbert/common';

const ref = new ReflectionClass(Test);
const methods = ref.getMethods(MethodFilter.Own | MethodFilter.Local);

for (const method of methods) {
	console.log(
		method.name,
		method.getParameters()
	);
}

ReflectionMethod

This class allows you to query information about a class method, its parameters, and its metadata. It also makes it simple to invoke the method on demand.

Generally, you will retrieve instances of this class using ReflectionClass.

import { ReflectionClass, MethodFilter } from '@baileyherbert/common';

const instance = new TargetClass();
const ref = new ReflectionClass(instance);
const methods = ref.getMethods(MethodFilter.Own | MethodFilter.Local);
const method = methods.find(m => m.hasMetadata('example'));

// Invoke the method on the instance
// The first parameter is required as "this"
// The remaining parameters are sent to the method as args
method.invoke(instance);

// You can also create a closure to call the method later
const closure = method.createClosure(instance);
closure(...args);

Native

Command

This is a utility class that helps run a command or process. It provides an interface that can make it easier to manage command line arguments, and offers simple events to listen for data or exit codes.

import { Command } from '@baileyherbert/common';

const command = new Command('ffmpeg');

command.setOption('-i', 'input_0.mp4');
command.setOption('-i', 'input_1.mp4');
command.setOption('-c', 'copy');
command.setOption('-map', '0:v:0');
command.setOption('-map', '1:a:0');
command.setFlag('-shortest');
command.setParameter('output.mp4');

// Listen for data
command.on('stderr', data => console.error(data));
command.on('stdout', data => console.log(data));

// The 'output' event combines both stderr and stdout
command.on('output', data => console.log(data));

// Start the process and wait for it to exit
const exitCode = await command.execute();

The class offers a logging option that will record all process output to an internal buffer. You can then read, write, and clear the logged output.

// Set logging to true before executing the command
command.logging = true;
await command.execute();

// Get all output so far as a Buffer
const output = command.getLog();

// Write output to a file
await command.writeLog('filename.txt');

// Clear the log
command.clearLog();

Decorators

@Reflectable

This is a blank decorator used to trigger decorator emit for reflection. It can be applied to both classes and methods.

import { Reflectable } from '@baileyherbert/common';

@Reflectable()
class Test {}

Types

Json

These types describe data that can be serialized into (or deserialized from) a JSON string.

import { Json, JsonMap, JsonArray } from '@baileyherbert/common';

Key<T>

This type is used to extract the keys from type, interface, or object T. It falls back to a generic string type if T is invalid or undefined.

import { Key } from '@baileyherbert/common';

Value<T, K, F>

This type is used to extract the value of index K from object T. However, if the object T is invalid or undefined, then fallback F is returned.

import { Value } from '@baileyherbert/common';

Fallback<T, F>

This type returns T if it is defined, or F otherwise.

import { Fallback } from '@baileyherbert/common';

Promisable<T>

This type joins T and Promise<T>.

import { Promisable } from '@baileyherbert/common';

Type<T>

This type represents the constructor of the given class T.

import { Type } from '@baileyherbert/common';

Action<T>

This type represents a function that accepts any arguments with an optional return type T (defaults to any).

import { Action } from '@baileyherbert/common';

About

[ARCHIVED] A collection of common utilities for my projects.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published