New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.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.0.0-rc.2 to 3.0.0-rc.4

__index__.ts

26

package.json
{
"name": "@travetto/base",
"displayName": "Base",
"version": "3.0.0-rc.2",
"description": "Application phase management, environment config and common utilities for travetto applications.",
"version": "3.0.0-rc.4",
"description": "Environment config and common utilities for travetto applications.",
"keywords": [

@@ -20,8 +19,7 @@ "shutdown-helper",

"files": [
"index.ts",
"bin",
"__index__.ts",
"src",
"support"
],
"main": "index.ts",
"main": "__index__.ts",
"repository": {

@@ -32,14 +30,20 @@ "url": "https://github.com/travetto/travetto.git",

"dependencies": {
"@travetto/boot": "^3.0.0-rc.0"
"@travetto/manifest": "^3.0.0-rc.3",
"@types/source-map-support": "^0.5.6",
"source-map-support": "^0.5.21"
},
"peerDependencies": {
"@travetto/cli": "^3.0.0-rc.2"
"@parcel/watcher": "^2.1.0",
"@travetto/transformer": "^3.0.0-rc.6"
},
"peerDependenciesMeta": {
"@travetto/cli": {
"@travetto/transformer": {
"optional": true
},
"@parcel/watcher": {
"optional": true
}
},
"docDependencies": {
"@travetto/config": true
"travetto": {
"displayName": "Base"
},

@@ -46,0 +50,0 @@ "private": false,

<!-- This file was generated by @travetto/doc and should not be modified directly -->
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/base/doc.ts and execute "npx trv doc" to rebuild -->
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/base/DOC.ts and execute "npx trv doc" to rebuild -->
# Base
## Application phase management, environment config and common utilities for travetto applications.
## Environment config and common utilities for travetto applications.

@@ -14,120 +14,27 @@ **Install: @travetto/base**

* Application Manifest
* File Operations
* Resource Management
* Lifecycle Support
* Environment Support
* Console Management
* Standard Error Support
* Stream Support
* Object Utilities
* Data Utilities
* Common Utilities
* Process Execution
* Shutdown Management
* Standard Error Support
* General Utilities
## Application Manifest
The framework provides basic environment information, e.g. in prod/test/dev. This is useful for runtime decisions. This is primarily used by the framework, but can prove useful to application developers
as well. The information that is available is:
## Environment Support
The functionality we support for testing and retrieving environment information:
* `env.prod`- Determines if app is in prod mode. A `boolean` flag that should indicate a production run.
* `env.name` - The environment name. Will usually be one of `dev`, `test`, or `prod`. Can be anything that is passed in.
* `env.profiles: Set<string>` - Specific application profiles that have been activated. This is useful for indicating different configuration or run states.
* `env.debug` - Simple logging flag. This `boolean` flag will enable or disable logging at various levels. By default `debug` is on in non-`prod`.
* `env.resources: string[]` - Resource folders. Search paths for resolving resource requests via [ResourceManager](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts)
* `source.local: string[]` - Local source folders for transpiling. Does not extend to installed modules.
* `source.common: string[]` - Common source folders for transpiling. Includes installed modules.
* `hasProfile(p: string): boolean;` - Test whether or not a profile is active.
* `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
* `getInt(key: string, def?: number): number;` - Retrieve an environmental value as a number
* `getList(key: string): string[];` - Retrieve an environmental value as a list
## File Operations
The framework does a fair amount of file system scanning to auto - load files. It also needs to have knowledge of what files are available. The framework provides a simple and performant functionality for recursively finding files. This functionality leverages regular expressions in lieu of glob pattern matching(this is to minimize overall code complexity).
A simple example of finding specific `.config` files in your codebase:
**Code: Looking for all .config files with the prefix defined by svc**
```typescript
import * as fs from 'fs/promises';
import { PathUtil, ScanFs } from '@travetto/boot';
export async function processServiceConfigs(svc: string) {
const svcConfigs = await ScanFs.scanDir({ testFile: f => new RegExp(`${svc}.*[.]config$/`).test(f) }, PathUtil.cwd);
for (const conf of svcConfigs) {
// Do work
const contents = await fs.readFile(conf.module, 'utf8');
}
}
```
## Resource Management
Resource management, loading of files, and other assets at runtime is a common pattern that the [ResourceManager](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts) encapsulates. It provides the ability to add additional search paths, as well as resolve resources by searching in all the registered paths.
**Code: Finding Images**
```typescript
import { ResourceManager } from '@travetto/base';
/**
* Find a single image, first one wins by resource path order
*/
export async function findSingleImage() {
const imagePath = await ResourceManager.find('/images/asset.gif');
return imagePath;
}
/**
* Find all .gif files under the images folder
*/
export async function findAllImages() {
const imagePaths = await ResourceManager.findAll(/[.]gif$/, 'images/');
return imagePaths;
}
```
## Lifecycle Support
During the lifecycle of an application, there is a need to handle different phases of execution. When executing a phase, the code will recursively find all `phase.<phase>.ts` files under `node_modules/@travetto`, and in the root of your project. The format of each phase handler is comprised of five main elements:
* The phase of execution, which is defined by the file name `phase.<phase>.ts`
* The key of the handler to be referenced for dependency management.
* The list of dependent handlers that the current handler depends on, if any.
* The list of handlers that should be dependent on the current handler, if any.
* The actual functionality to execute
An example would be something like `phase.init.ts` in the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Environment-aware config management using yaml files") module.
**Code: Config phase init**
```typescript
/**
* Initializes the config source
*/
export const init = {
key: '@trv:config/init',
before: ['@trv:registry/init'],
async action(): Promise<void> {
const { ConfigManager } = await import('../src/manager');
await ConfigManager.init();
}
};
```
## 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**
```typescript
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
});
}
```
## 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](https://github.com/travetto/travetto/tree/main/module/base/src/error.ts#L7) (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](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") module and HTTP status codes).
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](https://github.com/travetto/travetto/tree/main/module/base/src/error.ts#L13) (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](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") module and HTTP status codes).
The [AppError](https://github.com/travetto/travetto/tree/main/module/base/src/error.ts#L7) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
The [AppError](https://github.com/travetto/travetto/tree/main/module/base/src/error.ts#L13) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:

@@ -143,55 +50,89 @@ * `general` - General purpose errors

### Stacktrace
The built in stack filtering will remove duplicate or unnecessary lines, as well as filter out framework specific steps that do not aid in debugging. The final result should be a stack trace that is concise and clear.
## Console Management
From a test scenario:
This module provides logging functionality, built upon [console](https://nodejs.org/api/console.html) operations.
**Code: Tracking asynchronous behavior**
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](https://github.com/travetto/travetto/tree/main/module/base/src/console.ts). This module, hooks into the [ConsoleManager](https://github.com/travetto/travetto/tree/main/module/base/src/console.ts) and receives all logging events from all files compiled by the [Travetto](https://travetto.dev).
A sample of the instrumentation would be:
**Code: Sample logging at various levels**
```typescript
import { StacktraceUtil } from '@travetto/base';
export function work() {
console.debug('Start Work');
function inner3() {
throw new Error('Uh oh');
try {
1 / 0;
} catch (err) {
console.error('Divide by zero', { error: err });
}
console.debug('End Work');
}
```
async function inner2() {
return await inner3();
**Code: Sample After Transpilation**
```javascript
"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"));
function work() {
ᚕ_c.log({ level: "debug", source: __filename, line: 2, scope: "work", args: ['Start Work'] });
try {
1 / 0;
}
catch (err) {
ᚕ_c.log({ level: "error", source: __filename, line: 7, scope: "work", args: ['Divide by zero', { error: err }] });
}
ᚕ_c.log({ level: "debug", source: __filename, line: 9, scope: "work", args: ['End Work'] });
}
exports.work = work;
```
async function inner1() {
return await inner2();
}
### Filtering Debug
async function test() {
await inner1();
}
The `debug` messages can be filtered using the patterns from the [debug](https://www.npmjs.com/package/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.
export function main() {
process.on('unhandledRejection', (err: unknown) => {
console.log(StacktraceUtil.simplifyStack(err as Error));
});
test();
}
**Terminal: Sample environment flags**
```bash
# 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
```
Will produce the following stack trace:
Additionally, the logging framework will merge [debug](https://www.npmjs.com/package/debug) into the output stream, and supports the standard usage
**Terminal: Stack trace from async errors**
**Terminal: Sample environment flags for standard usage**
```bash
$ node @travetto/base/bin/main ./doc/stack-test.ts
Error: Uh oh
at inner3 (./doc/stack-test.ts:4:9)
at inner2 (./doc/stack-test.ts:8:16)
at inner1 (./doc/stack-test.ts:12:16)
at test (./doc/stack-test.ts:16:9)
at Object.main (./doc/stack-test.ts:24:3)
# Debug
$ DEBUG=express:*,@travetto/rest npx trv run rest
```
The needed functionality cannot be loaded until `init.action` executes, and so must be required only at that time.
## Stream Support
The [StreamUtil](https://github.com/travetto/travetto/tree/main/module/base/src/stream.ts#L11) class provides basic stream utilities for use within the framework:
## General Utilities
Simple functions for providing a minimal facsimile to [lodash](https://lodash.com), but without all the weight. Currently [Util](https://github.com/travetto/travetto/tree/main/module/base/src/util.ts#L34) includes:
* `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.
## Object Utilities
Simple functions for providing a minimal facsimile to [lodash](https://lodash.com), but without all the weight. Currently [ObjectUtil](https://github.com/travetto/travetto/tree/main/module/base/src/object.ts#L10) includes:

@@ -203,2 +144,7 @@ * `isPrimitive(el)` determines if `el` is a `string`, `boolean`, `number` or `RegExp`

* `isSimple(a)` determines if `a` is a simple value
## Data Utilities
Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/base/src/data.ts#L9) 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:

@@ -208,34 +154,50 @@ * `loose`, which is the default is the most lenient. It will not error out, and overwrites will always happen

* `strict`, will error out if the types do not match
## Common Utilities
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/base/src/util.ts#L14) 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 '!'.
## CLI - build
## 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#L95) 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.
**Terminal: Build usage**
```bash
$ trv build --help
A simple example would be:
Usage: build [options]
**Code: Running a directory listing via ls**
```typescript
import { ExecUtil } from '@travetto/base';
Options:
-o, --output <output> Output directory
-q, --quiet Quiet operation
-h, --help display help for command
export async function executeListing() {
const { result } = ExecUtil.spawn('ls');
const final = await result;
console.log('Listing', { lines: final.stdout.split('\n') });
}
```
This command line operation pre-compiles all of the application source code. You can target the output location as well, which is useful in conjunction with `process.env.TRV_CACHE` for relocating the compiled files.
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 also supports:
## CLI - clean
* `timeout` as the number of milliseconds the process can run before terminating and throwing an error
* `quiet` which suppresses all stdout/stderr output
* `stdin` as a string, buffer or stream to provide input to the program you are running;
* `timeoutKill` allows for registering functionality to execute when a process is force killed by timeout
The module provides the ability to clear the compilation cache to handle any inconsistencies that may arise.
## Shutdown Management
**Terminal: Clean operation**
```bash
$ trv clean --help
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.
Usage: clean [options]
As a registered shutdown handler, you can do.
Options:
-q, --quiet Quiet operation
-h, --help display help for command
**Code: Registering a shutdown handler**
```typescript
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
});
}
```

@@ -1,23 +0,11 @@

import { SourceUtil } from '@travetto/boot/src/internal/source-util';
import util from 'util';
import { AppManifest } from './manifest';
import { RootIndex } from '@travetto/manifest';
export type LogLevel = 'info' | 'warn' | 'debug' | 'error';
import type { ConsoleListener, ConsoleEvent, LogLevel } from './types';
type LineContext = { file: string, line: number };
interface ConsoleListener {
onLog<T extends LineContext>(context: LogLevel, ctx: T, args: unknown[]): void;
}
function setGlobal<K extends string | symbol>(ctx: Partial<Record<K, unknown>>, key: K, val: unknown): void {
ctx[key] = val;
}
const CONSOLE_RE = /(\bconsole[.](debug|info|warn|log|error)[(])|\n/g;
function wrap(target: Console): ConsoleListener {
return {
onLog(level: LogLevel, ctx: LineContext, args: unknown[]): void {
return target[level](...args);
onLog(ev: ConsoleEvent): void {
return target[ev.level](...ev.args);
}

@@ -27,3 +15,24 @@ };

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

@@ -37,3 +46,3 @@ *

/**
* Stack of nested appenders
* Stack of nested listeners
*/

@@ -43,54 +52,53 @@ #stack: ConsoleListener[] = [];

/**
* The current appender
* The current listener
*/
#appender: ConsoleListener;
#listener: ConsoleListener;
/**
* List of log levels to exclude
* List of logging filters
*/
readonly #exclude = new Set<string>([]);
#filters: Partial<Record<LogLevel, (x: ConsoleEvent) => boolean>> = {};
/**
* Unique key to use as a logger function
*/
constructor(public readonly key: string) {
setGlobal(globalThis, this.key, this.invoke.bind(this));
this.#exclude = new Set();
if (AppManifest.env.debug.status === false) {
this.#exclude.add('debug');
}
async register(): Promise<this> {
this.set(console); // Init to console
SourceUtil.addPreProcessor(this.#instrument.bind(this)); // Register console manager
this.setDebugFromEnv();
await initNpmDebug(this);
return this;
}
/**
* Modify typescript file to point to the Console Manager
* Add exclusion
* @private
*/
#instrument(filename: string, fileContents: string): string {
// Insert filename into all log statements for all components
let line = 1;
fileContents = fileContents.replace(CONSOLE_RE, (a, cmd, lvl) => {
if (a === '\n') {
line += 1;
return a;
} else {
lvl = lvl === 'log' ? 'info' : lvl;
return `${this.key}('${lvl}', { file: ᚕsrc(__filename), line: ${line} },`; // Make ConsoleManager target for all console invokes
filter(level: LogLevel, filter?: boolean | ((ctx: ConsoleEvent) => boolean)): void {
if (filter !== undefined) {
if (typeof filter === 'boolean') {
const v = filter;
filter = (): boolean => v;
}
});
return fileContents;
this.#filters[level] = filter;
} else {
delete this.#filters[level];
}
}
setDebugFromEnv(): void {
const notProd = !/prod/i.test(process.env.NODE_ENV ?? '');
this.setDebug(process.env.DEBUG ?? (notProd ? '@' : false));
}
/**
* Add exclusion
* @private
* Set logging debug level
*/
exclude(val: string, add = true): void {
if (add) {
this.#exclude.add(val);
setDebug(debug: boolean | string): void {
const isSet = debug !== undefined && debug !== '';
const isFalse = typeof debug === 'boolean' ? !debug : /^(0|false|no|off)/i.test(debug);
if (isSet && !isFalse) {
const active = RootIndex.getModuleList('local', typeof debug === 'string' ? debug : '');
active.add('@npm:debug');
this.filter('debug', ctx => active.has(ctx.module));
} else {
this.#exclude.delete(val);
this.filter('debug', () => false);
}

@@ -102,12 +110,23 @@ }

*/
invoke(level: LogLevel, ctx: LineContext, ...args: unknown[]): void {
if (this.#exclude.has(level)) {
invoke(ev: ConsoleEvent): void {
// Resolve input to source file
const source = ev.source ? RootIndex.getSourceFile(ev.source) : RootIndex.mainModule.output;
const mod = RootIndex.getModuleFromSource(source);
const outEv = {
...ev,
timestamp: new Date(),
source,
module: ev.module ?? mod?.name,
modulePath: ev.modulePath ?? (mod ? source.split(`${mod.source}/`)[1] : '')
};
if (this.#filters[outEv.level] && !this.#filters[outEv.level]!(outEv)) {
return; // Do nothing
} else {
return this.#listener.onLog(outEv);
}
return this.#appender.onLog(level, ctx, args);
}
/**
* Set a new console appender, works as a stack to allow for nesting
* Set a new console listener, works as a stack to allow for nesting
*/

@@ -121,3 +140,3 @@ set(cons: ConsoleListener | Console, replace = false): void {

}
this.#appender = this.#stack[0];
this.#listener = this.#stack[0];
}

@@ -131,3 +150,3 @@

this.#stack.shift();
this.#appender = this.#stack[0];
this.#listener = this.#stack[0];
}

@@ -137,2 +156,3 @@ }

export const ConsoleManager = new $ConsoleManager('ᚕlg');
export const ConsoleManager = new $ConsoleManager();
export const log = ConsoleManager.invoke.bind(ConsoleManager);

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

import { ErrorCategory } from './internal/error';
export { ErrorCategory } from './internal/error'; // Re-export
export type ErrorCategory =
'general' |
'notfound' |
'data' |
'permissions' |
'authentication' |
'timeout' |
'unavailable';

@@ -36,3 +42,3 @@ /**

*/
override toJSON(extra: Record<string, unknown> = {}): unknown {
toJSON(): unknown {
const out: Record<string, unknown> = {

@@ -44,7 +50,2 @@ message: this.message,

};
for (const [key, value] of Object.entries(extra)) {
if (!(key in out)) {
out[key] = value;
}
}
if (this.payload) {

@@ -51,0 +52,0 @@ for (const [key, value] of Object.entries(this.payload)) {

@@ -1,153 +0,126 @@

import * as fs from 'fs/promises';
import { Readable } from 'stream';
import { createReadStream } from 'fs';
import { Readable } from 'stream';
import fs from 'fs/promises';
import { PathUtil, ScanFs, ScanEntry, FsUtil } from '@travetto/boot';
import { path, RootIndex } from '@travetto/manifest';
import { AppError } from './error';
import { AppManifest } from './manifest';
import { GlobalEnv } from './global-env';
const cleanPath = (p: string): string => p.charAt(0) === '/' ? p.substring(1) : p;
export type ResourceDescription = { size: number, path: string };
/**
* Standard resource management interface allowing for look up by resource name
* across multiple resource paths
* Primary contract for resource handling
*/
class $ResourceManager {
#cache = new Map<string, string>();
#paths: string[] = [];
#rootPaths: string[];
constructor(rootPaths: string[]) {
this.#rootPaths = rootPaths;
this.#init();
}
#init(): void {
this.#paths.push(...this.#rootPaths);
this.#paths = this.#paths
.map(x => PathUtil.resolveUnix(x))
.filter(x => FsUtil.existsSync(x));
}
export interface ResourceProvider {
/**
* Consume Scan entry into indexing all resources available
* Describe the resource
* @param pth The path to resolve
*/
#scanEntry(base: string, found: Set<string>, out: string[], r: ScanEntry): void {
if (ScanFs.isDir(r)) {
if (r.children) {
for (const el of r.children!) {
this.#scanEntry(base, found, out, el);
}
}
return;
}
const p = `${base}/${r.module}`;
if (!found.has(p)) {
found.add(p);
out.push(p);
this.#cache.set(p, r.file);
}
}
describe(pth: string): Promise<ResourceDescription>;
/**
* Add a new search path
* @param searchPath Path to look through
* @param full Is the path fully qualified or should it be relative to the cwd
* Read a resource, mimicking fs.read
* @param pth The path to read
*/
addPath(searchPath: string, index = -1): void {
if (index < 0) {
this.#paths.push(PathUtil.resolveUnix(searchPath));
} else {
this.#paths.splice(index, 0, PathUtil.resolveUnix(searchPath));
}
}
read(pth: string, binary?: false): Promise<string>;
read(pth: string, binary: true): Promise<Buffer>;
read(pth: string, binary?: boolean): Promise<string | Buffer>;
/**
* List all paths
* Read a resource as a stream, mimicking fs.readStream
* @param pth The path to read
*/
getPaths(): string[] {
return this.#paths.slice(0);
readStream(pth: string, binary?: boolean): Promise<Readable>;
}
/**
* Simple file-based resource provider
*/
export class FileResourceProvider implements ResourceProvider {
#paths: string[];
#rawPaths: string[];
moduleFolder?: string;
mainFolder?: string;
maxDepth = 1000;
constructor(paths: string[]) {
this.#rawPaths = paths;
}
/**
* List all paths as relative to the cwd
*/
getRelativePaths(): string[] {
return this.#paths.slice(0).map(x => x.replace(`${PathUtil.cwd}/`, ''));
#getModulePath(mod: string, rel?: string): string {
return path.resolve(RootIndex.getModule(mod)!.source, rel ?? '');
}
/**
* Provide an absolute path for a resource identifier
* @param rel The relative path of a resource
*/
async findAbsolute(rel: string): Promise<string> {
rel = cleanPath(rel);
await this.find(rel);
return this.#cache.get(rel)!;
#getPaths(): string[] {
const main = RootIndex.manifest.mainModule;
return this.#paths ??= this.#rawPaths.map(pth => {
const [base, sub] = pth.replace(/^@$/, main).replace(/^@#/, `${main}#`).split('#');
return RootIndex.hasModule(base) ?
this.#getModulePath(base, sub ?? (base !== main ? this.moduleFolder : undefined) ?? this.mainFolder) :
path.resolve(base, sub ?? this.mainFolder ?? '');
});
}
/**
* Find a given resource and return it's location
* @param pth The relative path of a resource to find
*/
async find(pth: string): Promise<string> {
pth = cleanPath(pth);
if (this.#cache.has(pth)) {
return this.#cache.get(pth)!;
}
for (const f of this.#paths.map(x => PathUtil.joinUnix(x, pth))) {
if (await FsUtil.exists(f)) {
this.#cache.set(pth, f);
return f;
async #getPath(file: string): Promise<string> {
for (const sub of this.#getPaths()) {
const resolved = path.join(sub, file);
if (await fs.stat(resolved).catch(() => false)) {
return resolved;
}
}
throw new AppError(`Unable to find: ${file}, searched=${this.#getPaths().join(',')}`, 'notfound');
}
throw new AppError(`Cannot find resource: ${pth}, searched: ${this.#paths}`, 'notfound');
getAllPaths(): string[] {
return this.#getPaths().slice(0);
}
/**
* Read a resource, mimicking fs.read
* @param pth The path to read
*/
async read(pth: string): Promise<Buffer>;
async read(pth: string, options?: 'utf8' | 'utf-8' | { encoding: 'utf8' | 'utf-8' }): Promise<string>;
/**
* Read a resource, mimicking fs.read
* @param pth The path to read
* @param options The options to determine the read behavior
*/
async read(pth: string, options?: Parameters<typeof fs.readFile>[1]): Promise<string | Buffer> {
pth = await this.find(pth);
return fs.readFile(pth, options);
async describe(file: string): Promise<ResourceDescription> {
file = await this.#getPath(file);
const stat = await fs.stat(file);
return { size: stat.size, path: file };
}
/**
* Read a resource as a stream, mimicking fs.readStream
* @param pth The path to read
* @param options The options to determine the read behavior
*/
async readStream(pth: string, options?: Parameters<typeof createReadStream>[1]): Promise<Readable> {
pth = await this.find(pth);
return createReadStream(pth, options);
async read(file: string, binary?: false): Promise<string>;
async read(file: string, binary: true): Promise<Buffer>;
async read(file: string, binary = false): Promise<string | Buffer> {
file = await this.#getPath(file);
return fs.readFile(file, binary ? undefined : 'utf8');
}
async readStream(file: string, binary = true): Promise<Readable> {
file = await this.#getPath(file);
return createReadStream(file, binary ? undefined : 'utf8');
}
/**
* Find all resources by a specific pattern
* @param pattern Pattern to search against
* @param base The base folder to start searching from
* Query using a simple predicate, looking for files recursively
*/
async findAll(pattern: RegExp, base: string = ''): Promise<string[]> {
async query(filter: (file: string) => boolean, hidden = false, maxDepth = this.maxDepth): Promise<string[]> {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const search = [...this.#getPaths().map(x => [x, x, 0] as [string, string, number])];
const seen = new Set();
const out: string[] = [];
const found = new Set<string>();
for (const root of this.#paths) {
const results = await ScanFs.scanDir({ testFile: x => pattern.test(x) },
PathUtil.resolveUnix(root, base));
for (const r of results) {
this.#scanEntry(base, found, out, r);
while (search.length) {
const [folder, root, depth] = search.shift()!;
for (const sub of await fs.readdir(folder).catch(() => [])) {
if (sub === '.' || sub === '..' || (!hidden && sub.startsWith('.'))) {
continue;
}
const resolved = path.resolve(folder, sub);
const stats = await fs.stat(resolved);
if (stats.isDirectory()) {
if (depth + 1 < maxDepth) {
search.push([resolved, root, depth + 1]);
}
} else {
const rel = resolved.replace(`${root}/`, '');
if (!seen.has(rel) && filter(rel)) {
out.push(rel);
seen.add(rel);
}
}
}

@@ -157,4 +130,19 @@ }

}
/**
* Query for first matching
*/
async queryFirst(filter: (file: string) => boolean, hidden = false, maxDepth = this.maxDepth): Promise<string | undefined> {
const [res] = await this.query(filter, hidden, maxDepth);
return res;
}
}
export const ResourceManager = new $ResourceManager(AppManifest.env.resources);
/**
* Simple file resource provider that relies on trv_resources
*/
export class CommonFileResourceProvider extends FileResourceProvider {
constructor(paths: string[] = [...GlobalEnv.resourcePaths, path.resolve('resources')]) {
super(paths);
}
}
import { setTimeout } from 'timers/promises';
import { ModuleUtil } from '@travetto/boot/src/internal/module-util';
import { RootIndex } from '@travetto/manifest';
import { Env } from './env';
import { Util } from './util';
import { AppManifest } from './manifest';
const ogExit = process.exit;
export type Closeable = {
close(cb?: Function): unknown;
name?: string;
};

@@ -18,2 +13,6 @@

function isPromise(a: unknown): a is Promise<unknown> {
return !!a && (a instanceof Promise || (typeof a === 'object') && 'then' in a);
}
/**

@@ -35,2 +34,3 @@ * Shutdown manager, allowing for hooks into the shutdown process.

#unhandled: UnhandledHandler[] = [];
#exit = process.exit;

@@ -51,12 +51,17 @@ async #getAvailableListeners(exitCode: number): Promise<unknown[]> {

try {
console.debug('Starting', { name });
if (name) {
console.debug('Starting', { name });
}
const res = handler();
if (Util.isPromise(res)) {
if (isPromise(res)) {
// If a promise, queue for handling
promises.push(res);
res
.then(() => console.debug('Completed', { name }))
.catch((err: unknown) => console.error('Failed', { error: err, name }));
if (name) {
res.then(() => console.debug('Completed', { name }));
}
res.catch((err: unknown) => console.error('Failed', { error: err, name }));
} else {
console.debug('Completed', { name });
if (name) {
console.debug('Completed', { name });
}
}

@@ -75,3 +80,3 @@ } catch (err) {

if (exitCode > 0) { // Handle force kill
ogExit(exitCode);
this.#exit(exitCode);
} else {

@@ -84,6 +89,8 @@ return;

const name = RootIndex.mainPackage.name;
try {
// If the err is not an exit code
if (exitErr && typeof exitErr !== 'number') {
console.warn('Error on shutdown', { package: AppManifest.info.name, error: exitErr });
if (exitErr && typeof exitErr !== 'number' && exitErr !== 'SIGTERM' && exitErr !== 'SIGKILL') {
console.warn('Error on shutdown', { package: name, error: exitErr });
}

@@ -96,5 +103,6 @@

if (promises.length) {
const waitTime = Env.getInt('TRV_SHUTDOWN_WAIT', 2000);
const finalRun = Promise.race([
...promises,
Util.wait(AppManifest.env.shutdownWait).then(() => { throw new Error('Timeout on shutdown'); })
setTimeout(waitTime).then(() => { throw new Error('Timeout on shutdown'); })
]);

@@ -105,7 +113,7 @@ await finalRun;

} catch (err) {
console.warn('Error on shutdown', { package: AppManifest.info.name, error: err });
console.warn('Error on shutdown', { package: name, error: err });
}
if (this.#shutdownCode >= 0) {
ogExit(this.#shutdownCode);
this.#exit(this.#shutdownCode);
}

@@ -122,2 +130,14 @@ }

/**
* Execute unhandled behavior
*/
executeUnhandled(err: Error, value?: Promise<unknown>): void {
for (const handler of this.#unhandled) {
if (handler(err, value)) {
return;
}
}
this.execute(1, err);
}
/**
* Hook into the process to override the shutdown behavior

@@ -131,32 +151,19 @@ */

process.on('SIGTERM', this.execute.bind(this, 143));
process.on('uncaughtException', (err: Error) => this.#unhandled.find(x => !!x(err)));
process.on('unhandledRejection', (err: Error, p) => this.#unhandled.find(x => !!x(err, p)));
this.#unhandled.push(this.execute.bind(this, 1));
process.on('uncaughtException', this.executeUnhandled.bind(this));
process.on('unhandledRejection', this.executeUnhandled.bind(this));
}
/**
* Register to handle closeable on shutdown
* @param closeable
*/
onShutdown(closeable: Closeable): void;
/**
* Register a shutdown handler
* @param name Name to log
* @param handler Actual code
* @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(name: string, handler: Function, final?: boolean): void;
onShutdown(nameOrCloseable: string | Closeable, handler?: Function, final = false): void {
let name: string;
if (typeof nameOrCloseable !== 'string') {
name = nameOrCloseable.name ?? nameOrCloseable.constructor.name;
handler = nameOrCloseable.close.bind(nameOrCloseable);
} else {
name = nameOrCloseable;
handler = handler!;
onShutdown(src: undefined | string | Function | { constructor: Function }, handler: Function | Closeable, final: boolean = false): () => void {
if ('close' in handler) {
handler = handler.close.bind(handler);
}
if (/[.][jt]s$/.test(name)) {
name = ModuleUtil.getId(name);
}
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);
}

@@ -189,34 +196,8 @@

/**
* Listen for an unhandled event, as a promise
*/
listenForUnhandled(): Promise<never> & { cancel: () => void } {
const uncaught = Util.resolvablePromise<never>();
const uncaughtWithCancel: typeof uncaught & { cancel?: () => void } = uncaught;
const cancel = this.onUnhandled(err => { setTimeout(1).then(() => uncaught.reject(err)); return true; }, 0);
uncaughtWithCancel.cancel = (): void => {
cancel(); // Remove the handler
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
uncaughtWithCancel.resolve(undefined as never); // Close the promise
};
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return uncaughtWithCancel as (Promise<never> & { cancel: () => void });
exit(codeOrError: number | Error & { code?: number }): Promise<void> {
const code = typeof codeOrError === 'number' ? codeOrError : (codeOrError?.code ?? 1);
return this.executeAsync(code);
}
/**
* Wraps a function to capture unhandled exceptions for a period of time.
* Converts uncaught exception to a thrown error
* @param fn The function to wrap
*/
async captureUnhandled<U>(fn: () => U): Promise<U> {
const uncaught = this.listenForUnhandled();
try {
return (await Promise.race([uncaught, fn()]));
} finally {
uncaught.cancel();
}
}
}
export const ShutdownManager = new $ShutdownManager();

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

export type ClassInstance<T = any> = T & {
constructor: ConcreteClass<T> & { ᚕid: string };
constructor: ConcreteClass<T> & { Ⲑid: string };
};
export type Primitive = number | boolean | string | Date | Error;
export type LogLevel = 'info' | 'warn' | 'debug' | 'error';
export type ConsoleEvent = {
timestamp: Date;
level: LogLevel;
source: string;
line: number;
module: string;
modulePath: string;
scope?: string;
args: unknown[];
};
export interface ConsoleListener {
onLog(ev: ConsoleEvent): void;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export type MethodDescriptor<R = any, V = unknown> = TypedPropertyDescriptor<(this: V, ...params: any[]) => R>;
export const TypedObject: {
keys<T = unknown, K extends keyof T = keyof T>(o: T): K[];
fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
} & ObjectConstructor = Object;

@@ -1,284 +0,152 @@

import * as crypto from 'crypto';
import * as timers from 'timers/promises';
import crypto from 'crypto';
import { EnvUtil } from '@travetto/boot';
import { Class, ClassInstance } from './types';
export type TemplatePrim = string | number | boolean | Date | RegExp;
const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
export type TemplateType<T extends string> = (values: TemplateStringsArray, ...keys: (Partial<Record<T, TemplatePrim>> | string)[]) => string;
const MIN = 1000 * 60;
const DAY = 24 * MIN * 60;
const TIME_UNITS = {
y: DAY * 365,
M: DAY * 30,
w: DAY * 7,
d: DAY,
h: MIN * 60,
m: MIN,
s: 1000,
ms: 1
};
export type TimeSpan = `${number}${keyof typeof TIME_UNITS}`;
export type TimeUnit = keyof typeof TIME_UNITS;
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 };
const AsyncGeneratorFunction = Object.getPrototypeOf(async function* () { });
const GeneratorFunction = Object.getPrototypeOf(function* () { });
const AsyncFunction = Object.getPrototypeOf(async function () { });
/**
* Common utilities for object detection/manipulation
* Grab bag of common utilities
*/
export class Util {
static #timePattern = new RegExp(`^(-?[0-9.]+)(${Object.keys(TIME_UNITS).join('|')})$`);
static #deepAssignRaw(a: unknown, b: unknown, mode: 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): unknown {
const isEmptyA = a === undefined || a === null;
const isEmptyB = b === undefined || b === null;
const isArrA = Array.isArray(a);
const isArrB = Array.isArray(b);
const isSimpA = !isEmptyA && this.isSimple(a);
const isSimpB = !isEmptyB && this.isSimple(b);
let ret: unknown;
if (isEmptyA || isEmptyB) { // If no `a`, `b` always wins
if (mode === 'replace' || b === null || !isEmptyB) {
ret = isEmptyB ? b : this.shallowClone(b);
} else if (!isEmptyA) {
ret = this.shallowClone(a);
} else {
ret = undefined;
static #match<T, K extends unknown[]>(
rules: { value: T, positive: boolean }[],
compare: (rule: T, ...compareInput: K) => boolean,
unmatchedValue: boolean,
...input: K
): boolean {
for (const rule of rules) {
if (compare(rule.value, ...input)) {
return rule.positive;
}
} else {
if (isArrA !== isArrB || isSimpA !== isSimpB) {
throw new Error(`Cannot merge differing types ${a} and ${b}`);
}
if (isArrB) { // Arrays
ret = a; // Write onto A
if (mode === 'replace') {
ret = b;
} else {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const retArr = ret as unknown[];
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const bArr = b as unknown[];
for (let i = 0; i < bArr.length; i++) {
retArr[i] = this.#deepAssignRaw(retArr[i], bArr[i], mode);
}
}
} else if (isSimpB) { // Scalars
const match = typeof a === typeof b;
ret = b;
if (!match) { // If types do not match
if (mode === 'strict') { // Bail on strict
throw new Error(`Cannot merge ${a} [${typeof a}] with ${b} [${typeof b}]`);
} else if (mode === 'coerce') { // Force on coerce
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ret = this.coerceType(b, (a as ClassInstance).constructor, false);
}
}
} else { // Object merge
ret = a;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const bObj = b as Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const retObj = ret as Record<string, unknown>;
for (const key of Object.keys(bObj)) {
retObj[key] = this.#deepAssignRaw(retObj[key], bObj[key], mode);
}
}
}
return ret;
return unmatchedValue;
}
static #allowDenyRuleInput<T>(
rule: (string | T | [value: T, positive: boolean] | [value: T]),
convert: (inputRule: string) => T
): { value: T, positive: boolean } {
return typeof rule === 'string' ?
{ value: convert(rule.replace(/^!/, '')), positive: !rule.startsWith('!') } :
Array.isArray(rule) ?
{ value: rule[0], positive: rule[1] ?? true } :
{ value: rule, positive: true };
}
/**
* Has to JSON
* @param o Object to check
* Simple check against allow/deny rules
* @param rules
*/
static hasToJSON = (o: unknown): o is { toJSON(): unknown } =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
!!o && 'toJSON' in (o as object);
static allowDenyMatcher<T, K extends unknown[]>(
rules: string | (string | T | [value: T, positive: boolean])[],
convert: (rule: string) => T,
compare: (rule: T, ...compareInput: K) => boolean,
cacheKey?: (...keyInput: K) => string
): (...input: K) => boolean {
/**
* Create regex from string, including flags
* @param input Convert input to a regex
*/
static toRegex(input: string | RegExp): RegExp {
if (input instanceof RegExp) {
return input;
} else if (REGEX_PAT.test(input)) {
const [, pat, mod] = input.match(REGEX_PAT) ?? [];
return new RegExp(pat, mod);
const rawRules = (Array.isArray(rules) ? rules : rules.split(/\s*,\s*/g));
const convertedRules = rawRules.map(rule => this.#allowDenyRuleInput(rule, convert));
const unmatchedValue = !convertedRules.some(r => r.positive);
if (convertedRules.length) {
if (cacheKey) {
const cache: Record<string, boolean> = {};
return (...input: K) =>
cache[cacheKey(...input)] ??= this.#match(convertedRules, compare, unmatchedValue, ...input);
} else {
return (...input: K) => this.#match(convertedRules, compare, unmatchedValue, ...input);
}
} else {
return new RegExp(input);
return () => true;
}
}
/**
* Coerce an input of any type to the class provided
* @param input Input value
* @param type Class to coerce to (String, Boolean, Number, Date, RegEx, Object)
* @param strict Should a failure to coerce throw an error?
*/
static coerceType(input: unknown, type: typeof String, strict?: boolean): string;
static coerceType(input: unknown, type: typeof Number, strict?: boolean): number;
static coerceType(input: unknown, type: typeof Boolean, strict?: boolean): boolean;
static coerceType(input: unknown, type: typeof Date, strict?: boolean): Date;
static coerceType(input: unknown, type: typeof RegExp, strict?: boolean): RegExp;
static coerceType<T>(input: unknown, type: Class<T>, strict?: boolean): T;
static coerceType(input: unknown, type: Class<unknown>, strict = true): unknown {
// Do nothing
if (input === null || input === undefined) {
return input;
} else if (!strict && type !== String && input === '') {
return undefined; // treat empty string as undefined for non-strings in non-strict mode
} else if (type && input instanceof type) {
return input;
}
static buildEdgeMap<T, U extends OrderedState<T>>(items: List<U>): Map<T, Set<T>> {
const edgeMap = new Map(items.map(x => [x.key, new Set(x.after ?? [])]));
switch (type) {
case Date: {
const res = typeof input === 'number' || /^[-]?\d+$/.test(`${input}`) ?
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
new Date(parseInt(input as string, 10)) : new Date(input as Date);
if (strict && Number.isNaN(res.getTime())) {
throw new Error(`Invalid date value: ${input}`);
// Build out edge map
for (const input of items) {
for (const bf of input.before ?? []) {
if (edgeMap.has(bf)) {
edgeMap.get(bf)!.add(input.key);
}
return res;
}
case Number: {
const res = `${input}`.includes('.') ? parseFloat(`${input}`) : parseInt(`${input}`, 10);
if (strict && Number.isNaN(res)) {
throw new Error(`Invalid numeric value: ${input}`);
}
return res;
const afterSet = edgeMap.get(input.key)!;
for (const el of input.after ?? []) {
afterSet.add(el);
}
case Boolean: {
const res = /^(true|yes|1|on)$/i.test(`${input}`);
if (strict && !/^(false|no|off|0|true|yes|on|1)$/i.test(`${input}`)) {
throw new Error(`Invalid boolean value: ${input}`);
}
return res;
}
case RegExp: {
if (typeof input === 'string') {
try {
return this.toRegex(input);
} catch {
if (strict) {
throw new Error(`Invalid regex: ${input}`);
} else {
return;
}
}
} else if (strict) {
throw new Error('Invalid regex type');
} else {
return;
}
}
case Object: {
if (!strict || this.isPlainObject(input)) {
return input;
} else {
throw new Error('Invalid object type');
}
}
case undefined:
case String: return `${input}`;
}
throw new Error(`Unknown type ${type.name}`);
return edgeMap;
}
/**
* Clone top level properties to a new object
* @param o Object to clone
* Produces a satisfied ordering for a list of orderable elements
*/
static shallowClone<T = unknown>(a: T): T {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Array.isArray(a) ? a.slice(0) : (this.isSimple(a) ? a : { ...(a as {}) })) as T;
}
static ordered<T, U extends OrderedState<T>>(items: List<U>): U[] {
const edgeMap = this.buildEdgeMap<T, U>(items);
/**
* Is a value of primitive type
* @param el Value to check
*/
static isPrimitive(el: unknown): el is (string | boolean | number | RegExp) {
const type = typeof el;
return el !== null && el !== undefined && (type === 'string' || type === 'boolean' || type === 'number' || el instanceof RegExp || el instanceof Date);
}
// Loop through all items again
const keys: T[] = [];
while (edgeMap.size > 0) {
/**
* Is a value a plain JS object, created using {}
* @param obj Object to check
*/
static isPlainObject(obj: unknown): obj is Record<string, unknown> {
return typeof obj === 'object' // separate from primitives
&& obj !== undefined
&& obj !== null // is obvious
&& obj.constructor === Object // separate instances (Array, DOM, ...)
&& Object.prototype.toString.call(obj) === '[object Object]'; // separate build-in like Math
}
// Find node with no dependencies
const key = [...edgeMap].find(([, after]) => after.size === 0)?.[0];
if (!key) {
throw new Error(`Unsatisfiable dependency: ${[...edgeMap.keys()]}`);
}
/**
* Is a value a function
* @param o Object to check
*/
static isFunction(o: unknown): o is Function {
const proto = o && Object.getPrototypeOf(o);
return proto && (proto === Function.prototype || proto === AsyncFunction || proto === AsyncGeneratorFunction || proto === GeneratorFunction);
}
// Store, and remove
keys.push(key);
edgeMap.delete(key);
/**
* Is a value a class
* @param o Object to check
*/
static isClass(o: unknown): o is Class {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return !!(o as object) && !!(o as { prototype: unknown }).prototype &&
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(o as { prototype: { constructor: unknown } }).prototype.constructor !== Object.getPrototypeOf(Function);
}
// Remove node from all other elements in `all`
for (const [, rem] of edgeMap) {
rem.delete(key);
}
}
/**
* Is simple, as a primitive, function or class
*/
static isSimple(a: unknown): a is Function | Class | string | number | RegExp | Date {
return this.isPrimitive(a) || this.isFunction(a) || this.isClass(a);
const inputMap = new Map(items.map(x => [x.key, x]));
return keys.map(k => inputMap.get(k)!);
}
/**
* Is an error object
* 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 isError(a: unknown): a is Error {
return !!a && (a instanceof Error || (typeof a === 'object' && 'message' in a && 'stack' in a));
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('');
}
};
}
/**
* Is a promise object
*/
static isPromise(a: unknown): a is Promise<unknown> {
return !!a && (a instanceof Promise || (typeof a === 'object') && 'then' in a);
}
/**
* Deep assign from b to a
* @param a The target
* @param b The source
* @param mode How the assignment should be handled
*/
static deepAssign<T, U>(a: T, b: U, mode: | 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): T & U {
if (!a || this.isSimple(a)) {
throw new Error(`Cannot merge onto a simple value, ${a}`);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return this.#deepAssignRaw(a, b, mode) as T & U;
}
/**
* Generate a random UUID

@@ -297,122 +165,23 @@ * @param len The length of the uuid to generate

/**
* Produce a promise that is externally resolvable
* Naive hashing
*/
static resolvablePromise<T = void>(): Promise<T> & PromiseResolver<T> {
let ops: PromiseResolver<T>;
const prom = new Promise<T>((resolve, reject) => ops = { resolve, reject });
return Object.assign(prom, ops!);
}
static naiveHash(text: string): number {
let hash = 5381;
/**
* Test to see if a string is valid for relative time
* @param val
*/
static isTimeSpan(val: string): val is TimeSpan {
return this.#timePattern.test(val);
}
/**
* 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 {
if (typeof amount === 'string') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
[, amount, unit] = amount.match(this.#timePattern) as [undefined, '1m', 'm'] ?? [undefined, amount, unit];
if (!TIME_UNITS[unit]) {
return NaN;
}
amount = amount.includes('.') ? parseFloat(amount) : parseInt(amount, 10);
for (let i = 0; i < text.length; i++) {
// eslint-disable-next-line no-bitwise
hash = (hash * 33) ^ text.charCodeAt(i);
}
return amount * TIME_UNITS[unit ?? 'ms'];
}
/**
* Returns a new date with `age` units into the future
* @param age Number of units to extend
* @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
*/
static timeFromNow(age: number | TimeSpan, unit?: TimeUnit): Date {
return new Date(Date.now() + this.timeToMs(age, unit));
return Math.abs(hash);
}
/**
* Wait for n units of time
* Produce a promise that is externally resolvable
*/
static wait(n: number | TimeSpan, unit?: TimeUnit): Promise<void> {
return timers.setTimeout(this.timeToMs(n, unit));
static resolvablePromise<T = void>(): Promise<T> & PromiseResolver<T> {
let ops: PromiseResolver<T>;
const prom = new Promise<T>((resolve, reject) => ops = { resolve, reject });
return Object.assign(prom, ops!);
}
/**
* 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 = EnvUtil.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);
}
static #match<T, K extends unknown[]>(
rules: { value: T, positive: boolean }[],
compare: (rule: T, ...compareInput: K) => boolean,
unmatchedValue: boolean,
...input: K
): boolean {
for (const rule of rules) {
if (compare(rule.value, ...input)) {
return rule.positive;
}
}
return unmatchedValue;
}
static #allowDenyRuleInput<T>(
rule: (string | T | [value: T, positive: boolean] | [value: T]),
convert: (inputRule: string) => T
): { value: T, positive: boolean } {
return typeof rule === 'string' ?
{ value: convert(rule.replace(/^!/, '')), positive: !rule.startsWith('!') } :
Array.isArray(rule) ?
{ value: rule[0], positive: rule[1] ?? true } :
{ value: rule, positive: true };
}
/**
* Simple check against allow/deny rules
* @param rules
*/
static allowDenyMatcher<T, K extends unknown[]>(
rules: string | (string | T | [value: T, positive: boolean])[],
convert: (rule: string) => T,
compare: (rule: T, ...compareInput: K) => boolean,
cacheKey?: (...keyInput: K) => string
): (...input: K) => boolean {
const rawRules = (Array.isArray(rules) ? rules : rules.split(/\s*,\s*/g));
const convertedRules = rawRules.map(rule => this.#allowDenyRuleInput(rule, convert));
const unmatchedValue = !convertedRules.some(r => r.positive);
if (convertedRules.length) {
if (cacheKey) {
const cache: Record<string, boolean> = {};
return (...input: K) =>
cache[cacheKey(...input)] ??= this.#match(convertedRules, compare, unmatchedValue, ...input);
} else {
return (...input: K) => this.#match(convertedRules, compare, unmatchedValue, ...input);
}
} else {
return () => true;
}
}
}
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