Install: @travetto/base
npm install @travetto/base
# or
yarn add @travetto/base
Base is the foundation of all Travetto applications. It is intended to be a minimal application set, as well as support for commonly shared functionality. It has support for the following key areas:
- Environment Support
- Runtime Flags
- Console Management
- Resource Access
- Standard Error Support
- Stream Utilities
- Object Utilities
- Common Utilities
- Time Utilities
- Process Execution
- Shutdown Management
The functionality we support for testing and retrieving environment information for known environment variables. They can be accessed directly on the Env object, and will return a scoped EnvProp, that is compatible with the property definition. E.g. only showing boolean related fields when the underlying flag supports true
or false
Code: Base Known Environment Flags
interface TravettoEnv {
/**
* The node environment we are running in
* @default development
*/
NODE_ENV: 'development' | 'production';
/**
* Outputs all console.debug messages, defaults to `local` in dev, and `off` in prod.
*/
DEBUG: boolean | string;
/**
* Environment to deploy, defaults to `NODE_ENV` if not `TRV_ENV` is not specified.
*/
TRV_ENV: string;
/**
* Special role to run as, used to access additional files from the manifest during runtime.
*/
TRV_ROLE: Role;
/**
* Whether or not to run the program in dynamic mode, allowing for real-time updates
*/
TRV_DYNAMIC: boolean;
/**
* The folders to use for resource lookup
*/
TRV_RESOURCES: string[];
/**
* The max time to wait for shutdown to finish after initial SIGINT,
* @default 2s
*/
TRV_SHUTDOWN_WAIT: TimeSpan | number;
/**
* The desired runtime module
*/
TRV_MODULE: string;
/**
* The location of the manifest file
* @default undefined
*/
TRV_MANIFEST: string;
/**
* trvc log level
*/
TRV_BUILD: 'none' | 'info' | 'debug' | 'error' | 'warn',
}
For a given EnvProp, we support the ability to access different properties as a means to better facilitate environment variable usage.
Code: EnvProp Shape
export class EnvProp<T> {
constructor(public readonly key: string) { }
/** Remove value */
clear(): void;
/** Export value */
export(val: T | undefined): Record<string, string>;
/** Read value as string */
get val(): string | undefined;
/** Read value as list */
get list(): string[] | undefined;
/** Add values to list */
add(...items: string[]): void;
/** Read value as int */
get int(): number | undefined;
/** Read value as boolean */
get bool(): boolean | undefined;
/** Read value as a time value */
get time(): number | undefined;
/** Determine if the underlying value is truthy */
get isTrue(): boolean;
/** Determine if the underlying value is falsy */
get isFalse(): boolean;
/** Determine if the underlying value is set */
get isSet(): boolean;
}
Env also provides some convenience methods for common flags used at runtime within the framework. These are wrappers around direct access to process.env
values with a little bit of logic sprinkled in.
Code: Provided Flags
export const Env = delegate({
/** Get name */
get name(): string | undefined {
return process.env.TRV_ENV || (!prod() ? RuntimeContext.workspace.defaultEnv : undefined);
},
/** Are we in development mode */
get production(): boolean {
return prod();
},
/** Is the app in dynamic mode? */
get dynamic(): boolean {
return IS_TRUE.test(process.env.TRV_DYNAMIC!);
},
/** Get debug value */
get debug(): false | string {
const val = process.env.DEBUG ?? '';
return (!val && prod()) || IS_FALSE.test(val) ? false : val;
}
});
The primary access patterns for resources, is to directly request a file, and to resolve that file either via file-system look up or leveraging the Manifest's data for what resources were found at manifesting time.
The FileLoader allows for accessing information about the resources, and subsequently reading the file as text/binary or to access the resource as a Readable
stream. If a file is not found, it will throw an AppError with a category of 'notfound'.
The ResourceLoader extends FileLoader and utilizes the Env's TRV_RESOURCES
information on where to attempt to find a requested resource.
While the framework is 100 % compatible with standard Error
instances, there are cases in which additional functionality is desired. Within the framework we use AppError (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the RESTful API module and HTTP status codes).
The AppError takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
-
general
- General purpose errors -
system
- Synonym forgeneral
-
data
- Data format, content, etc are incorrect. Generally correlated to bad input. -
permission
- Operation failed due to lack of permissions -
auth
- Operation failed due to lack of authentication -
missing
- Resource was not found when requested -
timeout
- Operation did not finish in a timely manner -
unavailable
- Resource was unresponsive
This module provides logging functionality, built upon console operations.
The supported operations are:
-
console.error
which logs at theERROR
level -
console.warn
which logs at theWARN
level -
console.info
which logs at theINFO
level -
console.debug
which logs at theDEBUG
level -
console.log
which logs at theINFO
level
Note: All other console methods are excluded, specifically trace
, inspect
, dir
, time
/timeEnd
All of the logging instrumentation occurs at transpilation time. All console.*
methods are replaced with a call to a globally defined variable that delegates to the ConsoleManager. This module, hooks into the ConsoleManager and receives all logging events from all files compiled by the Travetto.
A sample of the instrumentation would be:
Code: Sample logging at various levels
export function work() {
console.debug('Start Work');
try {
1 / 0;
} catch (err) {
console.error('Divide by zero', { error: err });
}
console.debug('End Work');
}
Code: Sample After Transpilation
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.work = void 0;
const tslib_1 = require("tslib");
const ᚕ_c = tslib_1.__importStar(require("@travetto/base/src/console.js"));
var ᚕf = "@travetto/base/doc/transpile.js";
function work() {
ᚕ_c.log({ level: "debug", source: ᚕf, line: 2, scope: "work", args: ['Start Work'] });
try {
1 / 0;
}
catch (err) {
ᚕ_c.log({ level: "error", source: ᚕf, line: 7, scope: "work", args: ['Divide by zero', { error: err }] });
}
ᚕ_c.log({ level: "debug", source: ᚕf, line: 9, scope: "work", args: ['End Work'] });
}
exports.work = work;
The debug
messages can be filtered using the patterns from the debug. You can specify wild cards to only DEBUG
specific modules, folders or files. You can specify multiple, and you can also add negations to exclude specific packages.
Terminal: Sample environment flags
# Debug
$ DEBUG=-@travetto/model npx trv run app
$ DEBUG=-@travetto/registry npx trv run app
$ DEBUG=@travetto/rest npx trv run app
$ DEBUG=@travetto/*,-@travetto/model npx trv run app
Additionally, the logging framework will merge debug into the output stream, and supports the standard usage
Terminal: Sample environment flags for standard usage
# Debug
$ DEBUG=express:*,@travetto/rest npx trv run rest
The StreamUtil class provides basic stream utilities for use within the framework:
-
toBuffer(src: Readable | Buffer | string): Promise<Buffer>
for converting a stream/buffer/filepath to a Buffer. -
toReadable(src: Readable | Buffer | string):Promise<Readable>
for converting a stream/buffer/filepath to a Readable -
writeToFile(src: Readable, out: string):Promise<void>
will stream a readable into a file path, and wait for completion.
Simple functions for providing a minimal facsimile to lodash, but without all the weight. Currently ObjectUtil includes:
-
isPrimitive(el)
determines ifel
is astring
,boolean
,number
orRegExp
-
isPlainObject(obj)
determines if the obj is a simple object -
isFunction(o)
determines ifo
is a simpleFunction
-
isClass(o)
determines ifo
is a class constructor -
isSimple(a)
determines ifa
is a simple value -
isPromise(a)
determines ifa
is a promise
Common utilities used throughout the framework. Currently Util includes:
-
uuid(len: number)
generates a simple uuid for use within the application. -
allowDenyMatcher(rules[])
builds a matching function that leverages the rules as an allow/deny list, where order of the rules matters. Negative rules are prefixed by '!'. -
naiveHash(text: string)
produces a fast, and simplistic hash. No guarantees are made, but performs more than adequately for framework purposes. -
shortHash(text: string)
produces a sha512 hash and returns the first 32 characters. -
fullHash(text: string, size?: number)
produces a full sha512 hash. -
resolvablePromise()
produces aPromise
instance with theresolve
andreject
methods attached to the instance. This is extremely useful for integrating promises into async iterations, or any other situation in which the promise creation and the execution flow don't always match up.
Code: Sample makeTemplate Usage
const tpl = makeTemplate((name: 'age'|'name', val) => `**${name}: ${val}**`);
tpl`{{age:20}} {{name: 'bob'}}</>;
// produces
'**age: 20** **name: bob**'
TimeUtil contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
Code: Time Utilities
export class TimeUtil {
/**
* Test to see if a string is valid for relative time
* @param val
*/
static isTimeSpan(val: string): val is TimeSpan;
/**
* Returns time units convert to ms
* @param amount Number of units to extend
* @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
*/
static timeToMs(amount: number | TimeSpan, unit?: TimeUnit): number;
/**
* Resolve time or span to possible time
*/
static resolveInput(value: number | string | undefined): number | undefined;
/**
* Returns a new date with `amount` units into the future
* @param amount Number of units to extend
* @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
*/
static timeFromNow(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Date;
/**
* Pretty print a delta between now and `time`, with auto-detection of largest unit
*/
static prettyDeltaSinceTime(time: number, unit?: TimeUnit): string;
/**
* Pretty print a delta, with auto-detection of largest unit
* @param delta The number of milliseconds in the delta
*/
static prettyDelta(delta: number, unit?: TimeUnit): string;
}
ExecUtil exposes getResult
as a means to wrap child_process's process object. This wrapper allows for a promise-based resolution of the subprocess with the ability to capture the stderr/stdout.
A simple example would be:
Code: Running a directory listing via ls
import { spawn } from 'node:child_process';
import { ExecUtil } from '@travetto/base';
export async function executeListing() {
const final = await ExecUtil.getResult(spawn('ls'));
console.log('Listing', { lines: final.stdout.split('\n') });
}
Another key lifecycle is the process of shutting down. The framework provides centralized functionality for running operations on graceful shutdown. Primarily used by the framework for cleanup operations, this provides a clean interface for registering shutdown handlers. The code intercepts SIGTERM
and SIGUSR2
, with a default threshold of 2 seconds. These events will start the shutdown process, but also clear out the pending queue. If a kill signal is sent again, it will complete immediately.
As a registered shutdown handler, you can do.
Code: Registering a shutdown handler
import { ShutdownManager } from '@travetto/base';
export function registerShutdownHandler() {
ShutdownManager.onGracefulShutdown(async () => {
// Do important work, the framework will wait until all async
// operations are completed before finishing shutdown
});
}