Skip to main content
Deno 2 is finally here šŸŽ‰ļø
Learn more

JsExt

Additional functions for JavaScript programming in practice.

Install

npm i @ayonli/jsext

Usages

import jsext from "@ayonli/jsext";
// Or in Deno
import jsext from "https://deno.land/x/ayonli_jsext/index.ts"; // since v0.5.0

Functions

And other functions in sub-packages.

jsext.try

function _try<E = Error, R = any, A extends any[] = any[]>(
    fn: (...args: A) => R,
    ...args: A
): [E | null, R];
function _try<E = Error, R = any, A extends any[] = any[]>(
    fn: (...args: A) => Promise<R>,
    ...args: A
): Promise<[E | null, R]>;

Invokes a regular function or an async function and renders its result in an [err, res] tuple.

Example

const [err, res] = _try(() => {
    // do something that may fail
});

Example (async)

let [err, res] = await _try(async () => {
    return await axios.get("https://example.org");
});

if (err) {
    res = (err as any)["response"];
}

function _try<E = Error, R = any>(job: Promise<R>): Promise<[E | null, R]>;

Resolves a promise and renders its result in an [err, res] tuple.

Example

let [err, res] = await _try(axios.get("https://example.org"));

if (err) {
    res = (err as any)["response"];
}

function _try<E = Error, T = any, A extends any[] = any[], TReturn = any, TNext = unknown>(
    fn: (...args: A) => Generator<T, TReturn, TNext>,
    ...args: A
): Generator<[E | null, T], [E | null, TReturn], TNext>;
function _try<E = Error, T = any, A extends any[] = any[], TReturn = any, TNext = unknown>(
    fn: (...args: A) => AsyncGenerator<T, TReturn, TNext>,
    ...args: A
): AsyncGenerator<[E | null, T], [E | null, TReturn], TNext>;

Invokes a generator function or an async generator function and renders its yield value and result in an [err, val] tuple.

Example

const iter = _try(function* () {
    // do something that may fail
});

for (const [err, val] of iter) {
    if (err) {
        console.error("something went wrong:", err);
    } else {
        console.log("current value:", val);
    }
}

Example (async)

const iter = _try(async function* () {
    // do something that may fail
});

for await (const [err, val] of iter) {
    if (err) {
        console.error("something went wrong:", err);
    } else {
        console.log("current value:", val);
    }
}

function _try<E = Error, T = any, TReturn = any, TNext = unknown>(
    gen: Generator<T, TReturn, TNext>
): Generator<[E | null, T], [E | null, TReturn], TNext>;
function _try<E = Error, T = any, TReturn = any, TNext = unknown>(
    gen: AsyncGenerator<T, TReturn, TNext>
): AsyncGenerator<[E | null, T], [E | null, TReturn], TNext>;

Resolves a generator or an async generator and renders its yield value and result in an [err, val] tuple.

Example

const iter = Number.sequence(1, 10);

for (const [err, val] of _try(iter)) {
    if (err) {
        console.error("something went wrong:", err);
    } else {
        console.log("current value:", val);
    }
}

Example (async)

async function* gen() {
    // do something that may fail
};

for await (const [err, val] of _try(gen())) {
    if (err) {
        console.error("something went wrong:", err);
    } else {
        console.log("current value:", val);
    }
}

jsext.func

function func<T, R = any, A extends any[] = any[]>(
    fn: (this: T, defer: (cb: () => void) => void, ...args: A) => R
): (this: T, ...args: A) => R;

Inspired by Golang, creates a function that receives a defer keyword which can be used to carry deferred jobs that will be run after the main function is complete.

Multiple calls of the defer keyword is supported, and the callbacks are called in the LIFO order. Callbacks can be async functions if the main function is an async function or an async generator function, and all the running procedures will be awaited.

Example

const getVersion = func(async (defer) => {
    const file = await fs.open("./package.json", "r");
    defer(() => file.close());

    const content = await file.readFile("utf8");
    const pkg = JSON.parse(content);

    return pkg.version as string;
});

jsext.wrap

function wrap<T, Fn extends (this: T, ...args: any[]) => any>(
    fn: Fn,
    wrapper: (this: T, fn: Fn, ...args: Parameters<Fn>) => ReturnType<Fn>
): Fn

Wraps a function inside another function and returns a new function that copies the original functionā€™s name and other properties.

Example

function log(text: string) {
    console.log(text);
}

const show = wrap(log, function (fn, text) {
    return fn.call(this, new Date().toISOString() + " " + text);
});

console.log(show.name); // log
console.log(show.length); // 1
console.assert(show.toString() === log.toString());

jsext.throttle

function throttle<T, Fn extends (this: T, ...args: any[]) => any>(
    handler: Fn,
    duration: number
): Fn;
function throttle<T, Fn extends (this: T, ...args: any[]) => any>(handler: Fn, options: {
    duration: number;
    /**
     * Use the throttle strategy `for` the given key, this will keep the result in a global
     * cache, binding new `handler` function for the same key will result in the same result
     * as the previous, unless the duration has passed. This mechanism guarantees that both
     * creating the throttled function in function scopes and overwriting the handler are
     * possible.
     */
    for?: any;
}): Fn;

Creates a throttled function that will only be run once in a certain amount of time.

If a subsequent call happens within the duration, the previous result will be returned and the handler function will not be invoked.

Example

const fn = throttle((input: string) => input, 1_000);
console.log(fn("foo")); // foo
console.log(fn("bar")); // foo

await Promise.sleep(1_000);
console.log(fn("bar")); // bar

Example (with key)

const out1 = await throttle(() => Promise.resolve("foo"), { duration: 1_000, for: "example" })();
console.log(out1); // foo

const out2 = await throttle(() => Promise.resolve("bar"), { duration: 1_000, for: "example" })();
console.log(out2); // foo

await Promise.sleep(1_000);
const out3 = await throttle(() => Promise.resolve("bar"), { duration: 1_000, for: "example" })();
console.log(out3); // bar

jsext.mixins

function mixins<T extends Constructor<any>, M extends any[]>(
    base: T,
    ...mixins: { [X in keyof M]: Constructor<M[X]> }
): T & Constructor<UnionToIntersection<FlatArray<M, 1>>>;
function mixins<T extends Constructor<any>, M extends any[]>(
    base: T,
    ...mixins: M
): T & Constructor<UnionToIntersection<FlatArray<M, 1>>>;

Returns an extended class that combines all mixin methods.

This function does not mutates the base class but create a pivot class instead.

Example

class Log {
    log(text: string) {
        console.log(text);
    }
}

class View {
    display(data: Record<string, any>[]) {
        console.table(data);
    }
}

class Controller extends mixins(View, Log) {
    constructor(readonly topic: string) {
        super();
    }
}

const ctrl = new Controller("foo");
ctrl.log("something is happening");
ctrl.display([{ topic: ctrl.topic, content: "something is happening" }]);

console.assert(isSubclassOf(Controller, View));
console.assert(!isSubclassOf(Controller, Log));

jsext.isSubclassOf

function isSubclassOf<T, B>(ctor1: Constructor<T>, ctor2: Constructor<B>): boolean;

Checks if a class is a subclass of another class.

Example

class Moment extends Date {}

console.assert(isSubclassOf(Moment, Date));
console.assert(isSubclassOf(Moment, Object)); // all classes are subclasses of Object

jsext.read

function read<I extends AsyncIterable<any>>(iterable: I): I;
function read(es: EventSource, options?: { event?: string; }): AsyncIterable<string>;
function read<T extends Uint8Array | string>(ws: WebSocket): AsyncIterable<T>;
function read<T>(target: EventTarget, eventMap?: {
    message?: string;
    error?: string;
    close?: string;
}): AsyncIterable<T>;
function read<T>(target: NodeJS.EventEmitter, eventMap?: {
    data?: string;
    error?: string;
    close?: string;
}): AsyncIterable<T>;

Wraps a source as an AsyncIterable object that can be used in the for...await... loop for reading streaming data.

Example (EventSource)

// listen to the `onmessage`
const sse = new EventSource("/sse/message");

for await (const msg of read(sse)) {
    console.log("receive message:", msg);
}

// listen to a specific event
const channel = new EventSource("/sse/broadcast");

for await (const msg of read(channel, { event: "broadcast" })) {
    console.log("receive message:", msg);
}

Example (WebSocket)

const ws = new WebSocket("/ws");

for await (const data of read(ws)) {
    if (typeof data === "string") {
        console.log("receive text message:", data);
    } else {
        console.log("receive binary data:", data);
    }
}

Example (EventTarget)

for await (const msg of read(self)) {
    console.log("receive message from the parent window:", msg);
}

Example (EventEmitter)

for await (const msg of read(process)) {
    console.log("receive message from the parent process:", msg);
}

jsext.run

function run<T, A extends any[] = any[]>(script: string, args?: A, options?: {
    /** If not set, invoke the default function, otherwise invoke the specified function. */
    fn?: string;
    /** Automatically abort the task when timeout (in milliseconds). */
    timeout?: number;
    /**
     * Instead of dropping the worker after the task has completed, keep it alive so that it can
     * be reused by other tasks.
     */
    keepAlive?: boolean;
    /**
     * Choose whether to use `worker_threads` or `child_process` for running the script.
     * The default setting is `worker_threads`.
     * 
     * In browser or Deno, this option is ignored and will always use the web worker.
     */
    adapter?: "worker_threads" | "child_process";
    /**
     * In browser, by default, the program loads the worker entry directly from GitHub,
     * which could be slow due to poor internet connection, we can copy the entry file
     * `bundle/worker-web.mjs` to a local path of our website and set this option to that path
     * so that it can be loaded locally.
     * 
     * Or, if the code is bundled, the program won't be able to automatically locate the entry
     * file in the file system, in such case, we can also copy the entry file
     * (`bundle/worker.mjs` for Node.js and Bun, `bundle/worker-web.mjs` for browser and Deno)
     * to a local directory and supply this option instead.
     */
    workerEntry?: string;
}): Promise<{
    workerId: number;
    /** Terminates the worker and abort the task. */
    abort(): Promise<void>;
    /** Retrieves the return value of the function that has been called.. */
    result(): Promise<T>;
    /** Iterates the yield value if the function returns a generator. */
    iterate(): AsyncIterable<T>;
}>;

Runs a script in a worker thread or child process that can be aborted during runtime.

In Node.js and Bun, the script can be either a CommonJS module or an ES module, and is relative to the current working directory if not absolute.

In browser and Deno, the script can only be an ES module, and is relative to the current URL (or working directory for Deno) if not absolute.

Example (result)

const job1 = await run("./job-example.mjs", ["World"]);
console.log(await job1.result()); // Hello, World

Example (iterate)

const job2 = await run<string, [string[]]>("./job-example.mjs", [["foo", "bar"]], {
    fn: "sequence",
});
for await (const word of job2.iterate()) {
    console.log(word);
}
// output:
// foo
// bar

Example (abort)

const job3 = await run<string, [string]>("./job-example.mjs", ["foobar"], {
    fn: "takeTooLong",
});
await job3.abort();
const [err, res] = await _try(job3.result());
console.assert(err === null);
console.assert(res === undefined);

jsext.example

function example<T, A extends any[] = any[]>(
    fn: (this: T, console: Console, ...args: A) => void | Promise<void>,
    options?: {
        /** Suppress logging to the terminal and only check the output. */
        suppress?: boolean;
    }
): (this: T, ...args: A) => Promise<void>;

Inspired by Golangā€™s Example as Test design, creates a function that carries example code with // output: comments, when the returned function is called, it will automatically check if the actual output matches the one declared in the comment.

The example function receives a customized console object which will be used to log outputs instead of using the built-in console.

NOTE: this function is used to simplify the process of writing tests, it does not work in Bun and browsers currently, because Bun hasnā€™t implement the Console constructor and removes comments during runtime, and the function relies on Node.js built-in modules.

Example

it("should output as expected", example(console => {
    console.log("Hello, World!");
    // output:
    // Hello, World!
}));

Types

  • AsyncFunction
  • AsyncGeneratorFunction
  • AsyncFunctionConstructor
  • Constructor<T>
  • TypedArray
  • Optional<T, K extends keyof T>
  • Ensured<T, K extends keyof T>

When augmenting, these types will be exposed to the global scope.

Sub-packages

string

import { compare, random, /* ... */ } from "@ayonli/jsext/string";
// or
import "@ayonli/jsext/string/augment";

Functions

  • compare(str1: string, str2: string): -1 | 0 | 1
  • random(length: number): string
  • count(str: string, sub: string): number
  • capitalize(str: string, all?: boolean): string
  • hyphenate(str: string): string
  • words(str: string): string[]
  • chunk(str: string, length: number): string[]
  • truncate(str: string, length: number): string
  • byteLength(str: string): number

Augmentation

  • String
    • compare(str1: string, str2: string): -1 | 0 | 1
    • random(length: number): string
    • prototype
      • count(sub: string): number
      • capitalize(all?: boolean): string
      • hyphenate(): string
      • words(): string[]
      • chunk(length: number): string[]
      • truncate(length: number): string
      • byteLength(): number

number

import { isFloat, isNumeric, /* ... */ } from "@ayonli/jsext/number";
// or
import "@ayonli/jsext/number/augment";

Functions

  • isFloat(value: unknown): boolean
  • isNumeric(value: unknown): boolean
  • isBetween(value: number, [min, max]: [number, number]): boolean
  • random(min: number, max: number): number
  • sequence(min: number, max: number, step?: number, loop?: boolean): Generator<number, void, unknown>

When augmenting, these functions will be attached to the Number constructor.

array

import { count, equals, /* ... */ } from "@ayonli/jsext/array";
// or
import "@ayonli/jsext/array/augment";

Functions

  • count<T>(arr: RealArrayLike<T>, ele: T): number
  • equals<T>(arr1: RealArrayLike<T>, arr2: RealArrayLike<T>): boolean
  • split<T>(arr: RealArrayLike<T>, delimiter: T): RealArrayLike<T>[]
  • chunk<T>(arr: RealArrayLike<T>, length: number): RealArrayLike<T>[]
  • uniq<T>(arr: T[]): T[]
  • shuffle<T>(arr: T[]): T[]
  • orderBy<T>(arr: T[], key: keyof T, order: "asc" | "desc" = "asc"): T[]
  • groupBy<T>(arr: T[], fn: (item: T, i: number) => string | symbol, type?: ObjectConstructor): Record<string | symbol, T[]>
  • groupBy<T, K extends string>(arr: T[], fn: (item: T, i: number) => K, type: MapConstructor): Map<K, T[]>

Augmentation

  • Array<T>
    • prototype
      • first(): T
      • last(): T
      • count(ele: T): number
      • equals(another: T[]): boolean
      • split(delimiter: T): T[][]
      • chunk(length: number): T[][]
      • uniq(): T[]
      • shuffle(): T[]
      • toShuffled(): T[]
      • toReversed(): T[]
      • toSorted(fn?: ((a: T, b: T) => number) | undefined): T[]
      • orderBy(key: keyof T, order?: "asc" | "desc"): T[]
      • groupBy(fn: (item: T, i: number) => string | symbol, type?: ObjectConstructor): Record<string | symbol, T[]>
      • groupBy<K>(fn: (item: T, i: number) => K, type: MapConstructor): Map<K, T[]>

uint8array

import { compare, equals, /* ... */ } from "@ayonli/jsext/uint8array";
// or
import "@ayonli/jsext/uint8array/augment";

Functions

  • compare(arr1: Uint8Array, arr2: Uint8Array): -1 | 0 | 1
  • equals(arr1: Uint8Array, arr2: Uint8Array): boolean
  • split<T extends Uint8Array>(arr: T, delimiter: number): T[]
  • chunk<T extends Uint8Array>(arr: T, length: number): T[]

Augmentation

  • Uint8Array
    • compare(arr1: Uint8Array, arr2: Uint8Array): -1 | 0 | 1
    • prototype
      • equals(another: Uint8Array): boolean
      • split(delimiter: number): this[]
      • chunk(length: number): this[]

object

import { hasOwn, hasOwnMethod, /* ... */ } from "@ayonli/jsext/object";
// or
import "@ayonli/jsext/object/augment";

Functions

  • hasOwn(obj: any, key: string | number | symbol): boolean
  • hasOwnMethod(obj: any, method: string | symbol): boolean
  • patch<T extends {}, U>(target: T, source: U): T & U
  • patch<T extends {}, U, V>(target: T, source1: U, source2: V): T & U & V
  • patch<T extends {}, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W
  • patch(target: object, ...sources: any[]): any
  • pick<T extends object, U extends keyof T>(obj: T, keys: U[]): Pick<T, U>
  • pick<T>(obj: T, keys: (string | symbol)[]): Partial<T>
  • omit<T extends object, U extends keyof T>(obj: T, keys: U[]): Omit<T, U>
  • omit<T>(obj: T, keys: (string | symbol)[]): Partial<T>
  • as(value: unknown, type: StringConstructor): string | null
  • as(value: unknown, type: NumberConstructor): number | null
  • as(value: unknown, type: BigIntConstructor): bigint | null
  • as(value: unknown, type: BooleanConstructor): boolean | null
  • as(value: unknown, type: SymbolConstructor): symbol | null
  • as<T>(value: unknown, type: Constructor<T>): T | null
  • isValid(value: unknown): boolean

When augmenting, these functions will be attached to the Object constructor.

math

import { sum, avg, /* ... */ } from "@ayonli/jsext/math";
// or
import "@ayonli/jsext/math/augment";

Functions

  • sum(...values: number[]): number
  • avg(...values: number[]): number
  • product(...values: number[]): number

When augmenting, these functions will be attached to the Math namespace.

promise

import { timeout, after, /* ... */ } from "@ayonli/jsext/promise";
// or
import "@ayonli/jsext/promise/augment";

Functions

  • timeout<T>(value: T | PromiseLike<T>, ms: number): Promise<T>
  • after<T>(value: T | PromiseLike<T>, ms: number): Promise<T>
  • sleep(ms: number): Promise<void>
  • until(test: () => boolean | Promise<boolean>): Promise<void>

When augmenting, these functions will be attached to the Promise constructor.

collections

import BiMap from "@ayonli/jsext/collections/BiMap";
import CiMap from "@ayonli/jsext/collections/CiMap";
// or
import { BiMap, CiMap } from "@ayonli/jsext/collections";
// or
import "@ayonli/jsext/collections/augment";

Types

  • BiMap<K, V> (extends Map<K, V>) Bi-directional map, keys and values are unique and map to each other.
    • prototype (additional)
      • getKey(value: V): K | undefined
      • hasValue(value: V): boolean
      • deleteValue(value: V): boolean
  • CiMap<K extends string, V> (implements Map<K, V>) Case-insensitive map, keys are case-insensitive.

When augmenting, these types will be exposed to the global scope.

error

import Exception from "@ayonli/jsext/error/Exception";
// or
import { Exception } from "@ayonli/jsext/error";
// or
import "@ayonli/jsext/error/augment";

Types

  • Exception (extends Error)
    • cause?: unknown
    • code: number

When augmenting, these types will be exposed to the global scope.

Functions

  • toObject<T extends Error>(err: T): { [x: string | symbol]: any; }
  • fromObject<T extends Error>(obj: { [x: string | symbol]: any; }): T

Augmentation

  • Error
    • toObject<T extends Error>(err: T): { [x: string | symbol]: any; }
    • fromObject<T extends Error>(obj: { [x: string | symbol]: any; }): T
    • prototype
      • toJSON(): { [x: string | symbol]: any; }

Import all sub-package augmentations at once

import "@ayonli/jsext/augment";

When to use augmentations

If weā€™re developing libraries and share them openly on NPM, in order to prevent collision, itā€™s better not to use augmentations, but use the corresponding functions from the sub-packages instead.

But if weā€™re developing private projects, using augmentations can save a lot of time, itā€™s easier to read and write, and make sense.

Web Support

When using this package in the browser, there are three ways to import this package.

  1. Import From node_modules

This is the same as above, but requires a module bundler such as webpack.

  1. Import ES Module
<script type="module">
    import jsext from "https://deno.land/x/ayonli_jsext/esm/index.js";
    import "https://deno.land/x/ayonli_jsext/esm/augment.js";
    // or sub-packages
    import { isFloat, isNumeric } from "https://deno.land/x/ayonli_jsext/esm/number/index.js";
    import "https://deno.land/x/ayonli_jsext/esm/number/augment.js";
</script>
  1. Include Bundle
<script src="https://deno.land/x/ayonli_jsext/bundle/index.js"></script>
<script>
    const jsext = window["@ayonli/jsext"];
    // this will also include the augmentations
<script>