Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@travetto/base

Package Overview
Dependencies
Maintainers
1
Versions
357
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@travetto/base

Environment config and common utilities for travetto applications.

  • 3.1.0-rc.1
  • Source
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

Base

Environment config and common utilities for travetto applications.

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
  • Shared Global Environment State
  • Console Management
  • Resource Access
  • Standard Error Support
  • Stream Utilities
  • Object Utilities
  • Data Utilities
  • Common Utilities
  • Time Utilities
  • Process Execution
  • Shutdown Management

Environment Support

The functionality we support for testing and retrieving environment information:

  • isTrue(key: string): boolean; - Test whether or not an environment flag is set and is true
  • isFalse(key: string): boolean; - Test whether or not an environment flag is set and is false
  • isSet(key:string): boolean; - Test whether or not an environment value is set (excludes: null, '', and undefined)
  • get(key: string, def?: string): string; - Retrieve an environmental value with a potential default
  • getBoolean(key: string, isValue?: boolean) - Retrieve an environmental value as a boolean. If isValue is provided, determine if the environment variable matches the specified value
  • getInt(key: string, def?: number): number; - Retrieve an environmental value as a number
  • getList(key: string): string[]; - Retrieve an environmental value as a list

Shared Global Environment State

GlobalEnv is a non-cached interface to the Env class with specific patterns defined. It provides access to common patterns used at runtime within the framework.

Code: GlobalEnv Shape

export const GlobalEnv = {
  /** Get environment name */
  get envName(): string;
  /** Get debug value */
  get debug(): string | undefined;
  /** Are we in production mode */
  get prod(): boolean;
  /** Is the app in dynamic mode? */
  get dynamic(): boolean;
  /** The list of the profiles */
  get profiles(): string[];
  /** Get list of resource paths */
  get resourcePaths(): string[];
  /** Is test */
  get test(): boolean;
  /** Get node version */
  get nodeVersion(): string;
  /** Export as plain object */
  toJSON(): Record<string, unknown>;
} as const;

The source for each field is:

  • envName - This is derived from process.env.TRV_ENV with a fallback of process.NODE_ENV
  • prod - This is true if envName is a prod value
  • dynamic - This is derived from process.env.TRV_DYNAMIC. This field reflects certain feature sets used throughout the framework.
  • profiles - This is a list derived from process.env.TRV_PROFILES. This can be checked at runtime to see if specific profiles are met. This primarily used in the framework to determine if the test profile is activated.
  • resourcePaths - This is a list derived from process.env.TRV_RESOURCES. This points to a list of folders that the FileResourceProvider will search against, by default.
  • test - This is true if profiles includes a value of test
  • nodeVersion - This is derived from process.version, and is used primarily for logging purposes In addition to reading these values, there is a defined method for setting/updating these values:

Code: GlobalEnv Update

export function defineGlobalEnv(cfg: GlobalEnvConfig = {}): void {
  const { set = {} } = cfg;
  const resources = [...cfg.resourcePaths ?? [], ...GlobalEnv.resourcePaths];
  const test = cfg.test ?? GlobalEnv.test;
  let debug = cfg.debug ?? GlobalEnv.debug;
  const env = cfg.envName ?? GlobalEnv.envName;
  const profiles = new Set([...GlobalEnv.profiles, ...(cfg.profiles ?? [])]);
  const isProd = /^prod/i.test(env);

  if (test) {
    profiles.add(TEST);
    debug = false;
  } else {
    profiles.delete(!isProd ? PROD : DEV);
    profiles.add(isProd ? PROD : DEV);
  }

  for (const [k, v] of Object.entries(set)) {
    (v === undefined || v === null) ? delete process.env[k] : process.env[k] = `${v}`;
  }

  process.env.TRV_ENV = env;
  process.env.NODE_ENV = isProd ? 'production' : 'development';
  process.env.TRV_DYNAMIC = `${cfg.dynamic ?? GlobalEnv.dynamic}`;
  process.env.DEBUG = `${debug}`;
  process.env.TRV_PROFILES = [...profiles].sort().join(',');
  process.env.TRV_RESOURCES = resources.join(',');
}

As you can see this method exists to update/change the process.env values so that the usage of GlobalEnv reflects these changes. This is primarily used in testing, or custom environment setups (e.g. CLI invocations for specific applications).

Resource Access

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.

Code: ResourceProvider contract

export interface ResourceProvider {
  /**
   * Describe the resource
   * @param pth The path to resolve
   */
  describe(pth: string): Promise<ResourceDescription>;

  /**
   * Read a resource, mimicking fs.read
   * @param pth The path to read
   */
  read(pth: string, binary?: false): Promise<string>;
  read(pth: string, binary: true): Promise<Buffer>;
  read(pth: string, binary?: boolean): Promise<string | Buffer>;

  /**
   * Read a resource as a stream, mimicking fs.readStream
   * @param pth The path to read
   */
  readStream(pth: string, binary?: boolean): Promise<Readable>;
}

The ResourceProvider 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'. This contract is fairly simple to fill out, and the predominant implementation is FileResourceProvider. This ResourceProvider will utilize the GlobalEnv's resourcePaths information on where to attempt to find a requested resource.

Scanning for Resources

Beyond directly asking for a resource, there a times where it is helpful to know what resources are available at runtime. This is primarily used during development, and is a discouraged pattern for production as assumptions about the file-system may be incorrect (or change without warning).

To that end, FileQueryProvider exists, and is a valid ResourceProvider. It also provides the query method, to allow for scanning/finding resources that match certain patterns. Additionally, this class also allows for watching all the resource folders. This again is helpful during development/compilation, but should not be used in production.

Standard Error Support

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 for general
  • 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

Console Management

This module provides logging functionality, built upon console operations.

The supported operations are:

  • console.error which logs at the ERROR level
  • console.warn which logs at the WARN level
  • console.info which logs at the INFO level
  • console.debug which logs at the DEBUG level
  • console.log which logs at the INFO level

Note: All other console methods are excluded, specifically trace, inspect, dir, time/timeEnd

How Logging is Instrumented

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;

Filtering Debug

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

Stream Utilities

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.
  • waitForCompletion(src: Readable, finish:()=>Promise<any>) will ensure the stream remains open until the promise finish produces is satisfied.
  • streamLines(file: string, ensureEmpty = false): AsyncIterable<string> will watch a file for any line changes, and produce those changes as asynchronous iterable stream. Functionally, this is equivalent to using the Unix tail operation on a file

Object Utilities

Simple functions for providing a minimal facsimile to lodash, but without all the weight. Currently ObjectUtil includes:

  • isPrimitive(el) determines if el is a string, boolean, number or RegExp
  • isPlainObject(obj) determines if the obj is a simple object
  • isFunction(o) determines if o is a simple Function
  • isClass(o) determines if o is a class constructor
  • isSimple(a) determines if a is a simple value
  • isPromise(a) determines if a is a promise

Data Utilities

Data utilities for binding values, and type conversion. Currently DataUtil includes:

  • deepAssign(a, b, mode ?) which allows for deep assignment of b onto a, the mode determines how aggressive the assignment is, and how flexible it is. mode can have any of the following values:

    • loose, which is the default is the most lenient. It will not error out, and overwrites will always happen
    • coerce, will attempt to force values from b to fit the types of a, and if it can't it will error out
    • strict, will error out if the types do not match
  • coerceType(input: unknown, type: Class<unknown>, strict = true) which allows for converting an input type into a specified type instance, or throw an error if the types are incompatible.

  • shallowClone<T = unknown>(a: T): T will shallowly clone a field

  • filterByKeys<T>(obj: T, exclude: (string | RegExp)[]): T will filter a given object, and return a plain object (if applicable) with fields excluded using the values in the exclude input

Common Utilities

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.
  • makeTemplate<T extends string>(wrap: (key: T, val: TemplatePrim) => string) produces a template function tied to the distinct string values that key supports.
  • resolvablePromise() produces a Promise instance with the resolve and reject 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**'

Time Utilities

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;
  /**
   * 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;
  /**
   * Wait for 'amount' units of time
   */
  static wait(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Promise<void>;
  /**
   * Get environment variable as time
   * @param key env key
   * @param def backup value if not valid or found
   */
  static getEnvTime(key: string, def?: number | TimeSpan): number;
  /**
   * 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;
}

Process Execution

Just like child_process, the ExecUtil exposes spawn and fork. These are generally wrappers around the underlying functionality. In addition to the base functionality, each of those functions is converted to a Promise structure, that throws an error on an non-zero return status.

A simple example would be:

Code: Running a directory listing via ls

import { ExecUtil } from '@travetto/base';

export async function executeListing() {
  const { result } = ExecUtil.spawn('ls');
  const final = await result;
  console.log('Listing', { lines: final.stdout.split('\n') });
}

As you can see, the call returns not only the child process information, but the Promise to wait for. Additionally, some common patterns are provided for the default construction of the child process. In addition to the standard options for running child processes, the module allows for the following execution options:

Code: Execution Options

export interface ExecutionOptions extends SpawnOptions {
  /**
   * Should an error be caught and returned as a result. Determines whether an exit code > 0 throws
   * an Error, or if it merely marks the process as completed, marking the result as invalid.
   */
  catchAsResult?: boolean;
  /**
   * Should the environment be isolated, or inherit from process.env
   */
  isolatedEnv?: boolean;
  /**
   * Built in timeout for any execution. The number of milliseconds the process can run before
   * terminating and throwing an error
   */
  timeout?: number;
  /**
   * Determines how to treat the stdout/stderr data.
   *  - 'text' will assume the output streams are textual, and will convert to unicode data.
   *  - 'text-stream' makes the same assumptions as 'text', but will only fire events, and will
   *        not persist any data.  This is really useful for long running programs.
   *  - 'binary' treats all stdout/stderr data as raw buffers, and will not perform any transformations.
   *  - 'raw' avoids touching stdout/stderr altogether, and leaves it up to the caller to decide.
   */
  outputMode?: 'raw' | 'binary' | 'text' | 'text-stream';
  /**
   * On stderr line.  Requires 'outputMode' to be either 'text' or 'text-stream'
   */
  onStdErrorLine?: (line: string) => void;
  /**
   * On stdout line.  Requires 'outputMode' to be either 'text' or 'text-stream'
   */
  onStdOutLine?: (line: string) => void;
  /**
   * The stdin source for the execution
   */
  stdin?: string | Buffer | Readable;
}

Shutdown Management

Another key lifecycle is the process of shutting down. The framework provides centralized functionality for running operations on shutdown. Primarily used by the framework for cleanup operations, this provides a clean interface for registering shutdown handlers. The code overrides process.exit to properly handle SIGKILL and SIGINT, with a default threshold of 3 seconds. In the advent of a SIGTERM signal, the code exits immediately without any cleanup.

As a registered shutdown handler, you can do.

Code: Registering a shutdown handler

import { ShutdownManager } from '@travetto/base';

export function registerShutdownHandler() {
  ShutdownManager.onShutdown('handler-name', async () => {
    // Do important work, the framework will wait until all async
    //   operations are completed before finishing shutdown
  });
}

Keywords

FAQs

Package last updated on 25 Mar 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc