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 - npm Package Compare versions

Comparing version 3.4.2 to 4.0.0-rc.0

src/trv.d.ts

5

__index__.ts

@@ -0,3 +1,3 @@

/// <reference path="./src/trv.d.ts" />
export * from './src/console';
export * from './src/compiler';
export * from './src/data';

@@ -8,3 +8,2 @@ export * from './src/error';

export * from './src/file-loader';
export * from './src/global-env';
export * from './src/resource';

@@ -16,3 +15,3 @@ export * from './src/shutdown';

export * from './src/object';
export * from './src/proxy';
export * from './src/watch';
export * from './src/util';

12

package.json
{
"name": "@travetto/base",
"version": "3.4.2",
"version": "4.0.0-rc.0",
"description": "Environment config and common utilities for travetto applications.",

@@ -29,9 +29,9 @@ "keywords": [

"dependencies": {
"@travetto/manifest": "^3.4.0",
"@types/node": "^20.9.2",
"@types/source-map-support": "^0.5.10",
"source-map-support": "^0.5.21"
"@travetto/manifest": "^4.0.0-rc.0",
"@types/node": "^20.10.3",
"debug": "^4.3.4",
"@types/debug": "^4.1.12"
},
"peerDependencies": {
"@travetto/transformer": "^3.4.2"
"@travetto/transformer": "^4.0.0-rc.0"
},

@@ -38,0 +38,0 @@ "peerDependenciesMeta": {

@@ -18,3 +18,3 @@ <!-- This file was generated by @travetto/doc and should not be modified directly -->

* Environment Support
* Shared Global Environment State
* Runtime Flags
* Console Management

@@ -32,59 +32,114 @@ * Resource Access

## 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
* `addToList(key: string, value: string): string[];` - Add an item to an environment value, ensuring uniqueness
The functionality we support for testing and retrieving environment information for known environment variables. They can be accessed directly on the [Env](https://github.com/travetto/travetto/tree/main/module/base/src/env.ts#L104) object, and will return a scoped [EnvProp](https://github.com/travetto/travetto/tree/main/module/base/src/env.ts#L8), that is compatible with the property definition. E.g. only showing boolean related fields when the underlying flag supports `true` or `false`
## Shared Global Environment State
[GlobalEnv](https://github.com/travetto/travetto/tree/main/module/base/src/global-env.ts#L9) is a non-cached interface to the [Env](https://github.com/travetto/travetto/tree/main/module/base/src/env.ts#L4) class with specific patterns defined. It provides access to common patterns used at runtime within the framework.
**Code: GlobalEnv Shape**
**Code: Base Known Environment Flags**
```typescript
export const GlobalEnv = {
/** Get environment name */
get envName(): string;
/** Get debug module expression */
get debug(): string | undefined;
/** Are we in development mode */
get devMode(): boolean;
/** Is the app in dynamic mode? */
get dynamic(): boolean;
/** Get list of resource paths */
get resourcePaths(): string[];
/** Is test */
get test(): boolean;
/** Get node major version */
get nodeVersion(): number;
/** Export as plain object */
toJSON(): Record<string, unknown>;
} as const;
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',
}
```
The source for each field is:
* `envName` - This is derived from `process.env.TRV_ENV` with a fallback of `process.env.NODE_ENV`
* `devMode` - This is true if `process.env.NODE_ENV` is dev* or test
* `dynamic` - This is derived from `process.env.TRV_DYNAMIC`. This field reflects certain feature sets used throughout the framework.
* `resourcePaths` - This is a list derived from `process.env.TRV_RESOURCES`. This points to a list of folders that the [ResourceLoader](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts#L9) will search against.
* `test` - This is true if `envName` is `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:
### Environment Property
For a given [EnvProp](https://github.com/travetto/travetto/tree/main/module/base/src/env.ts#L8), we support the ability to access different properties as a means to better facilitate environment variable usage.
**Code: GlobalEnv Update**
**Code: EnvProp Shape**
```typescript
export function defineEnv(cfg: EnvInit = {}): void {
const envName = (cfg.envName ?? readEnvName()).toLowerCase();
process.env.NODE_ENV = /^dev|development|test$/.test(envName) ? 'development' : 'production';
process.env.DEBUG = `${envName !== 'test' && (cfg.debug ?? GlobalEnv.debug ?? false)}`;
process.env.TRV_ENV = envName;
process.env.TRV_DYNAMIC = `${cfg.dynamic ?? GlobalEnv.dynamic}`;
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;
}
```
As you can see this method exists to update/change the `process.env` values so that the usage of [GlobalEnv](https://github.com/travetto/travetto/tree/main/module/base/src/global-env.ts#L9) reflects these changes. This is primarily used in testing, or custom environment setups (e.g. CLI invocations for specific applications).
### Runtime Flags
[Env](https://github.com/travetto/travetto/tree/main/module/base/src/env.ts#L104) 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**
```typescript
export const Env = delegate({
/** Get name */
get name(): string | undefined {
return process.env.TRV_ENV || (!prod() ? 'local' : 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;
}
});
```
## Resource Access

@@ -95,3 +150,3 @@ 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](https://github.com/travetto/travetto/tree/main/module/manifest#readme "Support for project indexing, manifesting, along with file watching")'s data for what resources were found at manifesting time.

The [ResourceLoader](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts#L9) extends [FileLoader](https://github.com/travetto/travetto/tree/main/module/base/src/file-loader.ts#L12) and utilizes the [GlobalEnv](https://github.com/travetto/travetto/tree/main/module/base/src/global-env.ts#L9)'s `resourcePaths` information on where to attempt to find a requested resource.
The [ResourceLoader](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts#L10) extends [FileLoader](https://github.com/travetto/travetto/tree/main/module/base/src/file-loader.ts#L12) and utilizes the [Env](https://github.com/travetto/travetto/tree/main/module/base/src/env.ts#L104)'s `TRV_RESOURCES` information on where to attempt to find a requested resource.

@@ -124,3 +179,3 @@ ## Standard Error Support

## 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](https://github.com/travetto/travetto/tree/main/module/base/src/console.ts#L44). This module, hooks into the [ConsoleManager](https://github.com/travetto/travetto/tree/main/module/base/src/console.ts#L44) and receives all logging events from all files compiled by the [Travetto](https://travetto.dev).
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](https://github.com/travetto/travetto/tree/main/module/base/src/console.ts#L16). This module, hooks into the [ConsoleManager](https://github.com/travetto/travetto/tree/main/module/base/src/console.ts#L16) and receives all logging events from all files compiled by the [Travetto](https://travetto.dev).

@@ -214,7 +269,8 @@ A sample of the instrumentation would be:

## Common Utilities
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/base/src/util.ts#L14) includes:
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/base/src/util.ts#L11) 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.
* `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 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.

@@ -231,3 +287,3 @@

## Time Utilities
[TimeUtil](https://github.com/travetto/travetto/tree/main/module/base/src/time.ts#L23) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
[TimeUtil](https://github.com/travetto/travetto/tree/main/module/base/src/time.ts#L19) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.

@@ -249,2 +305,6 @@ **Code: Time Utilities**

/**
* 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

@@ -256,12 +316,2 @@ * @param amount Number of units to extend

/**
* 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

@@ -279,3 +329,3 @@ */

## Process Execution
Just like [child_process](https://nodejs.org/api/child_process.html), the [ExecUtil](https://github.com/travetto/travetto/tree/main/module/base/src/exec.ts#L108) 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.
Just like [child_process](https://nodejs.org/api/child_process.html), the [ExecUtil](https://github.com/travetto/travetto/tree/main/module/base/src/exec.ts#L110) 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.

@@ -339,3 +389,3 @@ A simple example would be:

## 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.
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.

@@ -349,3 +399,3 @@ As a registered shutdown handler, you can do.

export function registerShutdownHandler() {
ShutdownManager.onShutdown('handler-name', async () => {
ShutdownManager.onGracefulShutdown(async () => {
// Do important work, the framework will wait until all async

@@ -352,0 +402,0 @@ // operations are completed before finishing shutdown

@@ -1,39 +0,11 @@

import util from 'util';
import util from 'node:util';
import debug from 'debug';
import { RootIndex } from '@travetto/manifest';
import { RuntimeIndex } from '@travetto/manifest';
import type { ConsoleListener, ConsoleEvent, LogLevel } from './types';
const FALSE_RE = /^(0|false|no|off)/i;
const DEBUG_OG = { formatArgs: debug.formatArgs, log: debug.log };
function wrap(target: Console): ConsoleListener {
return {
onLog(ev: ConsoleEvent): void {
return target[ev.level](...ev.args);
}
};
}
// TODO: Externalize?
/**
* Registers handler for `debug` module in npm ecosystem
* @param mgr
*/
async function initNpmDebug(mgr: $ConsoleManager): Promise<void> {
try {
const { default: debug } = await import('debug');
debug.formatArgs = function (args: string[]): void {
args.unshift(this.namespace);
args.push(debug.humanize(this.diff));
};
debug.log = (modulePath, ...args: string[]): void => mgr.invoke({
level: 'debug', module: '@npm:debug', modulePath,
args: [util.format(...args)], line: 0, source: '', timestamp: new Date()
});
} catch (err) {
// Do nothing
}
}
/**
* Provides a general abstraction against the console.* methods to allow for easier capture and redirection.

@@ -61,6 +33,6 @@ *

async register(): Promise<this> {
this.set(console); // Init to console
await initNpmDebug(this);
return this;
constructor(listener: ConsoleListener) {
this.set(listener, true);
this.enhanceDebug(true);
this.debug(false);
}

@@ -85,9 +57,28 @@

/**
* Enable/disable enhanced debugging
*/
enhanceDebug(active: boolean): void {
if (active) {
Error.stackTraceLimit = 50;
debug.formatArgs = function (args: string[]): void {
args.unshift(this.namespace);
args.push(debug.humanize(this.diff));
};
debug.log = (modulePath, ...args: string[]): void => this.invoke({
level: 'debug', module: '@npm:debug', modulePath,
args: [util.format(...args)], line: 0, source: '', timestamp: new Date()
});
} else {
Error.stackTraceLimit = 10;
debug.formatArgs = DEBUG_OG.formatArgs;
debug.log = DEBUG_OG.log;
}
}
/**
* Set logging debug level
*/
setDebug(debugModules: string | boolean | undefined, devMode: boolean = false): void {
const debug = `${debugModules}` || (devMode ? '@' : '') || 'false';
if (!FALSE_RE.test(debug)) {
const active = RootIndex.getModuleList('local', debug);
debug(value: false | string): void {
if (value !== false) {
const active = RuntimeIndex.getModuleList('local', value || '@');
active.add('@npm:debug');

@@ -105,4 +96,4 @@ this.filter('debug', ctx => active.has(ctx.module));

// Resolve input to source file
const source = ev.source ? RootIndex.getSourceFile(ev.source) : RootIndex.mainModule.outputPath;
const mod = RootIndex.getModuleFromSource(source);
const source = ev.source ? RuntimeIndex.getSourceFile(ev.source) : RuntimeIndex.mainModule.outputPath;
const mod = RuntimeIndex.getModuleFromSource(source);
const outEv = {

@@ -126,4 +117,3 @@ ...ev,

*/
set(cons: ConsoleListener | Console, replace = false): void {
cons = ('onLog' in cons) ? cons : wrap(cons);
set(cons: ConsoleListener, replace = false): void {
if (!replace) {

@@ -148,3 +138,3 @@ this.#stack.unshift(cons);

export const ConsoleManager = new $ConsoleManager();
export const ConsoleManager = new $ConsoleManager({ onLog: (ev): void => { console![ev.level](...ev.args); } });
export const log = ConsoleManager.invoke.bind(ConsoleManager);

@@ -1,96 +0,125 @@

/**
* Basic utils for reading environment variables
*/
export class Env {
/// <reference path="./trv.d.ts" />
/**
* Get, check for key as passed, as all upper and as all lowercase
* @param k The environment key to search for
* @param def The default value if the key isn't found
*/
static get<K extends string = string>(k: string, def: K): K;
static get<K extends string = string>(k: string, def?: K): K | undefined;
static get<K extends string = string>(k: string, def?: K | undefined): K | undefined {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (process.env[k] ??
process.env[k.toUpperCase()] ??
process.env[k.toLowerCase()] ??
def) as K;
}
import { TimeSpan, TimeUtil } from './time';
/**
* Add a value to as part of a comma-separated list
* @param k The environment key to add to
*/
static addToList(k: string, value: string): string[] {
const values = Env.getList(k) ?? [];
if (!values.includes(value)) {
values.push(value);
const IS_TRUE = /^(true|yes|on|1)$/i;
const IS_FALSE = /^(false|no|off|0)$/i;
export class EnvProp<T> {
constructor(public readonly key: string) { }
/** Set value according to prop type */
set(val: T | undefined | null): void {
if (val === undefined || val === null) {
delete process.env[this.key];
} else {
process.env[this.key] = Array.isArray(val) ? `${val.join(',')}` : `${val}`;
}
process.env[k] = values.join(',');
return values;
}
/**
* Read value as a comma-separated list
* @param k The environment key to search for
*/
static getList(k: string, def: string[]): string[];
static getList(k: string, def?: string[] | undefined): string[] | undefined;
static getList(k: string, def?: string[] | undefined): string[] | undefined {
const val = this.get(k);
/** Remove value */
clear(): void {
this.set(null);
}
/** Export value */
export(val: T | undefined): Record<string, string> {
return val === undefined || val === '' ? { [this.key]: '' } : { [this.key]: Array.isArray(val) ? `${val.join(',')}` : `${val}` };
}
/** Read value as string */
get val(): string | undefined { return process.env[this.key] || undefined; }
/** Read value as list */
get list(): string[] | undefined {
const val = this.val;
return (val === undefined || val === '') ?
def : ([...val.split(/[, ]+/g)]
.map(x => x.trim())
.filter(x => !!x));
undefined : val.split(/[, ]+/g).map(x => x.trim()).filter(x => !!x);
}
/**
* Read value as an integer
* @param k The environment key to search for
* @param def The default value if the key isn't found
*/
static getInt(k: string, def: number | string): number {
return parseInt(this.get(k, `${def}`) ?? '', 10);
/** Add values to list */
add(...items: string[]): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.set([... new Set([...this.list ?? [], ...items])] as T);
}
/**
* Read value as boolean
* @param k The environment key to search for
*/
static getBoolean(k: string, isValue: boolean): boolean;
static getBoolean(k: string): boolean | undefined;
static getBoolean(k: string, isValue?: boolean): boolean | undefined {
const val = this.get(k);
if (val === undefined || val === '') {
return isValue ? false : undefined;
}
const match = val.match(/^((?<TRUE>true|yes|1|on)|false|no|off|0)$/i);
return isValue === undefined ? !!match?.groups?.TRUE : !!match?.groups?.TRUE === isValue;
/** Read value as int */
get int(): number | undefined {
const vi = parseInt(this.val ?? '', 10);
return Number.isNaN(vi) ? undefined : vi;
}
/**
* Determine if value is set explicitly
* @param k The environment key to search for
*/
static isSet(k: string): boolean {
const val = this.get(k);
return val !== undefined && val !== '';
/** Read value as boolean */
get bool(): boolean | undefined {
const val = this.val;
return (val === undefined || val === '') ? undefined : IS_TRUE.test(val);
}
/**
* Read value as true
* @param k The environment key to search for
*/
static isTrue(k: string): boolean {
return this.getBoolean(k, true);
/** Read value as a time value */
get time(): number | undefined {
return TimeUtil.resolveInput(this.val);
}
/**
* Read value as false
* @param k The environment key to search for
*/
static isFalse(k: string): boolean {
return this.getBoolean(k, false);
/** Determine if the underlying value is truthy */
get isTrue(): boolean {
return IS_TRUE.test(this.val ?? '');
}
}
/** Determine if the underlying value is falsy */
get isFalse(): boolean {
return IS_FALSE.test(this.val ?? '');
}
/** Determine if the underlying value is set */
get isSet(): boolean {
const val = this.val;
return val !== undefined && val !== '';
}
}
type AllType = {
[K in keyof TravettoEnv]: Pick<EnvProp<TravettoEnv[K]>, 'key' | 'export' | 'val' | 'set' | 'clear' | 'isSet' |
(TravettoEnv[K] extends unknown[] ? 'list' | 'add' : never) |
(Extract<TravettoEnv[K], number> extends never ? never : 'int') |
(Extract<TravettoEnv[K], boolean> extends never ? never : 'bool' | 'isTrue' | 'isFalse') |
(Extract<TravettoEnv[K], TimeSpan> extends never ? never : 'time')
>
};
function delegate<T extends object>(base: T): AllType & T {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return new Proxy(base as AllType & T, {
get(target, prop): unknown {
return typeof prop !== 'string' ? undefined :
// @ts-expect-error
(prop in base ? base[prop] : target[prop] ??= new EnvProp(prop));
}
});
}
const prod = (): boolean => process.env.NODE_ENV === 'production';
/** Basic utils for reading known environment variables */
export const Env = delegate({
/** Get name */
get name(): string | undefined {
return process.env.TRV_ENV || (!prod() ? 'local' : 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;
}
});

@@ -1,8 +0,10 @@

import rl from 'readline';
import { ChildProcess, spawn, SpawnOptions } from 'child_process';
import { Readable } from 'stream';
import { SHARE_ENV, Worker, WorkerOptions } from 'worker_threads';
import rl from 'node:readline';
import { ChildProcess, spawn, SpawnOptions } from 'node:child_process';
import { Readable } from 'node:stream';
import { parentPort, SHARE_ENV, Worker, WorkerOptions } from 'node:worker_threads';
import { path } from '@travetto/manifest';
import { Env } from './env';
const MINUTE = (1000 * 60);

@@ -126,3 +128,3 @@

...(opts.isolatedEnv ? { PATH: process.env.PATH } : process.env),
TRV_DYNAMIC: '0', // Force dynamic to not cascade
...Env.TRV_DYNAMIC.export(false), // Force dynamic to not cascade
...(opts.env ?? {})

@@ -185,3 +187,3 @@ }

rl.createInterface(proc.stdout).on('line', line => {
options.onStdOutLine?.(line);
options.onStdOutLine?.(`${line}\n`);
if (options.outputMode !== 'text-stream') {

@@ -194,3 +196,3 @@ stdout.push(Buffer.from(line), Buffer.from('\n'));

rl.createInterface(proc.stderr).on('line', line => {
options.onStdErrorLine?.(line);
options.onStdErrorLine?.(`${line}\n`);
if (options.outputMode !== 'text-stream') {

@@ -254,6 +256,3 @@ stderr.push(Buffer.from(line), Buffer.from('\n'));

// If we are listening, and we become disconnected
if (process.send) {
process.on('disconnect', () => process.exit(0));
}
this.exitOnDisconnect();

@@ -359,2 +358,23 @@ for (; ;) {

}
/**
* Send response to caller
*/
static sendResponse(res: unknown, failure?: boolean): void {
parentPort?.postMessage(res);
if (process.connected && process.send) { process.send(res); }
if (res !== undefined) {
const msg = typeof res === 'string' ? res : (res instanceof Error ? res.stack : JSON.stringify(res));
process[!failure ? 'stdout' : 'stderr'].write(`${msg}\n`);
}
process.exitCode ??= !failure ? 0 : 1;
}
/**
* Exit child process on disconnect, in ipc setting
*/
static exitOnDisconnect(): void {
// Shutdown when ipc bridge is closed
process.once('disconnect', () => process.kill(process.pid, 'SIGTERM'));
}
}

@@ -1,6 +0,6 @@

import { createReadStream } from 'fs';
import { Readable } from 'stream';
import fs from 'fs/promises';
import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
import fs from 'node:fs/promises';
import { path, RootIndex } from '@travetto/manifest';
import { path, RuntimeIndex } from '@travetto/manifest';

@@ -14,10 +14,17 @@ import { AppError } from './error';

#searchPaths: string[];
#searchPaths: readonly string[];
static resolvePaths(paths: string[]): string[] {
return [...new Set(paths.map(x => RuntimeIndex.resolveModulePath(x)))];
}
constructor(paths: string[]) {
this.#searchPaths = paths.flat().map(x => RootIndex.resolveModulePath(x));
this.#searchPaths = Object.freeze(FileLoader.resolvePaths(paths));
}
get searchPaths(): string[] {
return this.#searchPaths.slice(0);
/**
* The paths that will be searched on resolve
*/
get searchPaths(): readonly string[] {
return this.#searchPaths;
}

@@ -30,3 +37,3 @@

async resolve(relativePath: string): Promise<string> {
for (const sub of this.#searchPaths) {
for (const sub of this.searchPaths) {
const resolved = path.join(sub, relativePath);

@@ -37,3 +44,3 @@ if (await fs.stat(resolved).catch(() => false)) {

}
throw new AppError(`Unable to find: ${relativePath}, searched=${this.#searchPaths.join(',')}`, 'notfound');
throw new AppError(`Unable to find: ${relativePath}, searched=${this.searchPaths.join(',')}`, 'notfound');
}

@@ -40,0 +47,0 @@

@@ -1,6 +0,7 @@

import { RootIndex } from '@travetto/manifest';
import { GlobalEnv } from './global-env';
import { Env } from './env';
import { FileLoader } from './file-loader';
const RES = Env.TRV_RESOURCES;
const COMMON = ['@#resources', '@@#resources'];
/**

@@ -10,15 +11,21 @@ * File-based resource loader

export class ResourceLoader extends FileLoader {
constructor(paths?: string[] | readonly string[]) {
super([...paths ?? [], ...RES.list ?? [], ...COMMON]);
}
}
static getSearchPaths(paths: string[] = []): string[] {
return [
...(paths ?? []).flat(),
...GlobalEnv.resourcePaths,
'@#resources', // Module root
...(RootIndex.manifest.monoRepo ? ['@@#resources'] : []) // Monorepo root
].map(x => RootIndex.resolveModulePath(x));
class $RuntimeResources extends FileLoader {
#env: string = '__';
#paths: readonly string[];
get searchPaths(): readonly string[] {
if (this.#env !== RES.val) {
this.#env = RES.val!;
this.#paths = Object.freeze(FileLoader.resolvePaths([...RES.list ?? [], ...COMMON]));
}
return this.#paths;
}
}
constructor(paths: string[] = []) {
super(ResourceLoader.getSearchPaths(paths));
}
}
/** Resources available at runtime, updates in realtime with changes to process.env.TRV_RESOURCES */
export const RuntimeResources = new $RuntimeResources([]);

@@ -1,227 +0,61 @@

import { setTimeout } from 'timers/promises';
import { parentPort } from 'worker_threads';
import timers from 'node:timers/promises';
import { RootIndex } from '@travetto/manifest';
import { Env } from './env';
import { ObjectUtil } from './object';
export type Closeable = {
close(cb?: Function): unknown;
};
type UnhandledHandler = (err: Error, prom?: Promise<unknown>) => boolean | undefined | void;
type Listener = { name: string, handler: Function, final?: boolean };
/**
* Shutdown manager, allowing for hooks into the shutdown process.
*
* On a normal shutdown signal (SIGINT, SIGTERM), the shutdown manager
* will start a timer, and begin executing the shutdown handlers.
*
* The handlers should be synchronous and fast, as once a threshold timeout
* has been hit, the application will force kill itself.
*
* If the application receives another SIGTERM/SIGINT while shutting down,
* it will shutdown immediately.
* Shutdown manager, allowing for listening for graceful shutdowns
*/
class $ShutdownManager {
#listeners: Listener[] = [];
#shutdownCode = -1;
#unhandled: UnhandledHandler[] = [];
#exit = process.exit;
#exitRequestHandlers: (() => (void | Promise<void>))[] = [];
export class ShutdownManager {
async #getAvailableListeners(exitCode: number): Promise<unknown[]> {
const promises: Promise<unknown>[] = [];
static #registered = false;
static #handlers: { name?: string, handler: () => Promise<void> }[] = [];
// Get valid listeners depending on lifecycle
const listeners = this.#listeners.filter(x => exitCode >= 0 || !x.final);
// Retain unused listeners for final attempt, if needed
this.#listeners = this.#listeners.filter(x => exitCode < 0 && x.final);
// Handle each listener
for (const listener of listeners) {
const { name, handler } = listener;
try {
if (name) {
console.debug('Starting', { name });
}
const res = handler();
if (ObjectUtil.isPromise(res)) {
// If a promise, queue for handling
promises.push(res);
if (name) {
res.then(() => console.debug('Completed', { name }));
}
res.catch((err: unknown) => console.error('Failed', { error: err, name }));
} else {
if (name) {
console.debug('Completed', { name });
}
}
} catch (err) {
console.error('Failed', { name, error: err });
}
}
return promises;
}
async executeAsync(exitCode: number = 0, exitErr?: unknown): Promise<void> {
if (this.#shutdownCode > 0) { // Killed twice
if (exitCode > 0) { // Handle force kill
this.#exit(exitCode);
} else {
return;
}
} else {
this.#shutdownCode = exitCode;
}
const name = RootIndex.mainModuleName;
try {
// If the err is not an exit code
if (exitErr && typeof exitErr !== 'number' && exitErr !== 'SIGTERM' && exitErr !== 'SIGKILL') {
console.warn('Error on shutdown', { package: name, error: exitErr });
}
// Get list of all pending listeners
const promises = await this.#getAvailableListeners(exitCode);
// Run them all, with the ability for the shutdown to preempt
if (promises.length) {
const waitTime = Env.getInt('TRV_SHUTDOWN_WAIT', 2000);
const finalRun = Promise.race([
...promises,
setTimeout(waitTime).then(() => { throw new Error('Timeout on shutdown'); })
]);
await finalRun;
}
} catch (err) {
console.warn('Error on shutdown', { package: name, error: err });
}
if (this.#shutdownCode >= 0) {
this.#exit(this.#shutdownCode);
}
}
/**
* Begin shutdown process with a given exit code and possible error
* On Shutdown requested
* @param name name to log for
* @param handler synchronous or asynchronous handler
*/
execute(exitCode: number = 0, err?: unknown): void {
this.executeAsync(exitCode, err); // Fire and forget
}
/**
* Execute unhandled behavior
*/
executeUnhandled(err: Error, value?: Promise<unknown>): void {
for (const handler of this.#unhandled) {
if (handler(err, value)) {
return;
static onGracefulShutdown(handler: () => Promise<void>, name?: string | { constructor: { Ⲑid: string } }): () => void {
if (!this.#registered) {
this.#registered = true;
const done = (): void => { this.gracefulShutdown(0); };
process.on('SIGUSR2', done).on('SIGTERM', done).on('SIGINT', done);
}
this.#handlers.push({ handler, name: typeof name === 'string' ? name : name?.constructor.Ⲑid });
return () => {
const idx = this.#handlers.findIndex(x => x.handler === handler);
if (idx >= 0) {
this.#handlers.splice(idx, 1);
}
}
this.execute(1, err);
};
}
/**
* Hook into the process to override the shutdown behavior
* Wait for graceful shutdown to run and complete
*/
register(): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
process.exit = this.execute.bind(this) as (() => never); // NOTE: We do not actually throw an error the first time, to allow for graceful shutdown
process.on('SIGINT', this.execute.bind(this, 130));
process.on('SIGTERM', this.execute.bind(this, 143));
process.on('SIGUSR2', this.requestExit.bind(this));
process.on('uncaughtException', this.executeUnhandled.bind(this));
process.on('unhandledRejection', this.executeUnhandled.bind(this));
}
static async gracefulShutdown(code?: number): Promise<void> {
if (this.#handlers.length) {
console.debug('Graceful shutdown: started');
/**
* Register a shutdown handler
* @param name Class/Function to log for
* @param handler Handler or Closeable
* @param final If this should be run an attempt to shutdown or only on the final shutdown
*/
onShutdown(src: undefined | string | Function | { constructor: Function }, handler: Function | Closeable, final: boolean = false): () => void {
if ('close' in handler) {
handler = handler.close.bind(handler);
}
const name = typeof src === 'undefined' ? '' : (typeof src === 'string' ? src : ('Ⲑid' in src ? src.Ⲑid : src.constructor.Ⲑid));
this.#listeners.push({ name, handler, final });
return () => this.#listeners.splice(this.#listeners.findIndex(e => e.handler === handler), 1);
}
const items = this.#handlers.splice(0, this.#handlers.length);
const handlers = Promise.all(items.map(({ name, handler }) => {
if (name) {
console.debug('Stopping', { name });
}
return handler().catch(err => {
console.error('Error shutting down', { name, err });
});
}));
/**
* Listen for unhandled exceptions
* @param handler Listener for all uncaught exceptions if valid
* @param position Handler list priority
*/
onUnhandled(handler: UnhandledHandler, position = -1): () => void {
if (position < 0) {
this.#unhandled.push(handler);
} else {
this.#unhandled.splice(position, 0, handler);
}
return this.removeUnhandledHandler.bind(this, handler);
}
await Promise.race([
timers.setTimeout(Env.TRV_SHUTDOWN_WAIT.time ?? 2000), // Wait 2s and then force finish
handlers,
]);
/**
* Remove handler for unhandled exceptions
* @param handler The handler to remove
*/
removeUnhandledHandler(handler: UnhandledHandler): void {
const index = this.#unhandled.indexOf(handler);
if (index >= 0) {
this.#unhandled.splice(index, 1);
console.debug('Graceful shutdown: completed');
}
}
/**
* Trigger exit based on a code or a passed in error
*/
exit(codeOrError: number | Error & { code?: number }): Promise<void> {
const code = typeof codeOrError === 'number' ? codeOrError : (codeOrError?.code ?? 1);
return this.executeAsync(code);
}
/**
* Trigger shutdown, and return response as requested
*/
exitWithResponse(res: unknown, failure = false): Promise<void> {
parentPort?.postMessage(res);
process.send?.(res);
if (res !== undefined) {
const msg = typeof res === 'string' ? res : (res instanceof Error ? res.stack : JSON.stringify(res));
process[!failure ? 'stdout' : 'stderr'].write(`${msg}\n`);
if (code !== undefined) {
process.exit(code);
}
return this.exit(!failure ? 0 : (res && res instanceof Error ? res : 1));
}
/**
* Listen for request for graceful exit
*/
onExitRequested(handler: Closeable | (() => void)): void {
if ('close' in handler) {
handler = handler.close.bind(handler);
}
this.#exitRequestHandlers.push(handler);
}
/**
* Indicates the program is over, allowing cleanup of any resources that could block process exit
*/
async requestExit(): Promise<void> {
await Promise.all(this.#exitRequestHandlers.map(x => x()));
await this.exit(process.exitCode ?? 0);
}
}
export const ShutdownManager = new $ShutdownManager();
}

@@ -1,4 +0,4 @@

import { createWriteStream } from 'fs';
import { PassThrough, Readable, Writable } from 'stream';
import { ReadableStream as WebReadableStream } from 'stream/web';
import { createWriteStream } from 'node:fs';
import { PassThrough, Readable, Writable } from 'node:stream';
import { ReadableStream as WebReadableStream } from 'node:stream/web';

@@ -5,0 +5,0 @@ import type { ExecutionState } from './exec';

@@ -1,5 +0,1 @@

import timers from 'timers/promises';
import { Env } from './env';
const MIN = 1000 * 60;

@@ -55,2 +51,15 @@ const DAY = 24 * MIN * 60;

/**
* Resolve time or span to possible time
*/
static resolveInput(value: number | string | undefined): number | undefined {
if (value === undefined) {
return value;
}
const val = (typeof value === 'string' && /\d+[a-z]+$/i.test(value)) ?
(this.isTimeSpan(value) ? this.timeToMs(value) : undefined) :
(typeof value === 'string' ? parseInt(value, 10) : value);
return Number.isNaN(val) ? undefined : val;
}
/**
* Returns a new date with `amount` units into the future

@@ -65,27 +74,2 @@ * @param amount Number of units to extend

/**
* Wait for 'amount' units of time
*/
static wait(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Promise<void> {
return timers.setTimeout(this.timeToMs(amount, unit));
}
/**
* 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 {
const val = Env.get(key);
let ms: number | undefined;
if (val) {
if (this.isTimeSpan(val)) {
ms = this.timeToMs(val);
} else if (!Number.isNaN(+val)) {
ms = +val;
}
}
return ms ?? (def ? this.timeToMs(def) : NaN);
}
/**
* Pretty print a delta between now and `time`, with auto-detection of largest unit

@@ -92,0 +76,0 @@ */

@@ -8,3 +8,3 @@ /* eslint-disable @typescript-eslint/no-explicit-any */

export type Primitive = number | boolean | string | Date | Error;
export type Primitive = number | boolean | string | Date;

@@ -11,0 +11,0 @@ export type LogLevel = 'info' | 'warn' | 'debug' | 'error';

@@ -1,10 +0,7 @@

import crypto from 'crypto';
import crypto from 'node:crypto';
export type TemplatePrim = string | number | boolean | Date | RegExp;
export type TemplateType<T extends string> = (values: TemplateStringsArray, ...keys: (Partial<Record<T, TemplatePrim>> | string)[]) => string;
type PromiseResolver<T> = { resolve: (v: T) => void, reject: (err?: unknown) => void };
type List<T> = T[] | readonly T[];
type OrderedState<T> = { after?: List<T>, before?: List<T>, key: T };
type MapFn<T, U> = (val: T, i: number) => U | Promise<U>;

@@ -118,36 +115,2 @@ /**

/**
* Creates a template function with ability to wrap values
* @example
* ```
* const tpl = Util.makeTemplate((key: 'title'|'subtitle', val:TemplatePrim) => `||${val}||`)
* tpl`${{title: 'Main Title'}} is ${{subtitle: 'Sub Title'}}`
* ```
*/
static makeTemplate<T extends string>(wrap: (key: T, val: TemplatePrim) => string): TemplateType<T> {
return (values: TemplateStringsArray, ...keys: (Partial<Record<T, TemplatePrim>> | string)[]) => {
if (keys.length === 0) {
return values[0];
} else {
const out = keys.map((el, i) => {
let final = el;
if (typeof el !== 'string') {
const subKeys = Object.keys(el);
if (subKeys.length !== 1) {
throw new Error('Invalid template variable, one and only one key should be specified');
}
const [k] = subKeys;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
final = wrap(k as T, el[k as T]!)!;
}
return `${values[i] ?? ''}${final ?? ''}`;
});
if (values.length > keys.length) {
out.push(values[values.length - 1]);
}
return out.join('');
}
};
}
/**
* Generate a random UUID

@@ -207,2 +170,23 @@ * @param len The length of the uuid to generate

}
/**
* Map an async iterable with various mapping functions
*/
static mapAsyncItr<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
static mapAsyncItr<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
static mapAsyncItr<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
static async * mapAsyncItr<T>(source: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
let idx = -1;
for await (const el of source) {
if (el !== undefined) {
idx += 1;
let m = el;
for (const fn of fns) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
m = (await fn(m, idx)) as typeof m;
}
yield m;
}
}
}
}
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