npm.io
1.0.37 • Published 5 months ago

nfkit

Licence
MIT
Version
1.0.37
Deps
0
Size
351 kB
Vulns
0
Weekly
0

nfkit

nfkit is a TypeScript utility package focused on practical backend/frontend engineering helpers: middleware dispatching, i18n pipeline, lightweight app context (DI-like), task scheduling, abortable proxies, and more.

Feature Count (public exports)

Based on index.ts, the package currently exposes 14 public entries:

  • 13 functional modules
  • 1 shared types module (types)
# Entry Purpose
1 workflow Lazy chained calls for property/method/function invocation
2 dual-object Dual-state object (sync object + PromiseLike)
3 workflow-dispatcher Multi-worker task scheduler with retry/backoff and dynamic worker ops
4 round-robin Round-robin selector
5 abortable AbortSignal-aware proxy wrapper
6 middleware-dispatcher Generic middleware dispatching (dynamic/proto variants included)
7 i18n i18n pipeline and dictionary lookup middleware
8 patch-string-in-object Deep string patching inside object graphs
9 observe-diff Object mutation diff observer (add/update/delete)
10 memorize Per-instance memoization decorator for methods/getters
11 may-be-array `T
12 app-context Lightweight app context (provide/use/start/get)
13 configurer Config loading/parsing helpers
14 types Shared type exports (Awaitable, etc.)

Installation

npm i nfkit

Quick Start

import {
  createAppContext,
  MiddlewareDispatcher,
  I18n,
  I18nLookupMiddleware,
  WorkflowDispatcher,
} from 'nfkit';

API and Usage

1) workflow

Builds a lazy chain. Execution starts only when you await (or call .then/.catch/.finally).

API

workflow<T>(source: T | Promise<T>): Chain<T>

Example

import { workflow } from 'nfkit';

class Client {
  count = 0;
  async connect() {
    return this;
  }
  inc(n: number) {
    this.count += n;
    return this;
  }
}

const client = new Client();
const value = await workflow(client).connect().inc(2).count;
// value === 2
2) dual-object

Creates a dual-state object:

  • sync access path for properties/methods
  • async object path via await obj
  • pending-safe deferred calls for selected async methods (asyncMethods)

API

type Dual<T> = T & PromiseLike<T>;
const DUAL_PENDING: unique symbol;
throwDualPending(): never;

dualizeAny<T>(
  sync: () => T,
  asyncFn: () => Promise<T>,
  options?: { asyncMethods?: readonly (keyof T)[] },
): Dual<T>

Example

import { dualizeAny } from 'nfkit';

type UserClient = {
  id: string;
  ping(): Promise<number>;
};

const c = dualizeAny<UserClient>(
  () => ({ id: 'cached', ping: async () => 1 }),
  async () => ({ id: 'remote', ping: async () => 42 }),
  { asyncMethods: ['ping'] as const },
);

const v = await c.ping(); // works even while object is pending
const obj = await c;
3) workflow-dispatcher

Asynchronous multi-worker dispatcher with:

  • global dispatch (auto worker selection)
  • worker-specific dispatch
  • retry + exponential backoff
  • runtime worker replace/add/remove

API

new WorkflowDispatcher<F extends (...args: any[]) => Promise<any>>(
  workersOrPromises: Array<F | Promise<F>>,
  options?: { maxAttempts?: number; backoffBaseMs?: number },
)

dispatch(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>
dispatchSpecific(index: number, ...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>
replaceWorker(index: number, fn: F): void
addWorker(fn: F): number
removeWorker(index: number): Promise<void>
snapshot(): WorkerSnapshot<F>[]
get pending(): number

Example

import { WorkflowDispatcher } from 'nfkit';

type Worker = (x: number) => Promise<string>;

const d = new WorkflowDispatcher<Worker>([
  async (x) => `A:${x}`,
  async (x) => `B:${x}`,
]);

const r1 = await d.dispatch(1);
const r2 = await d.dispatchSpecific(0, 2);
4) round-robin

Simple round-robin picker.

API

class RoundRobin<T> {
  constructor(items: T[]);
  next(): T;
}

Example

import { RoundRobin } from 'nfkit';

const rr = new RoundRobin(['a', 'b', 'c']);
rr.next(); // a
rr.next(); // b
rr.next(); // c
rr.next(); // a
5) abortable

Wraps an object/function with an AbortSignal-aware proxy. After abort, access/calls throw (or reject for Promise flows).

API

class AbortedError extends Error {}

type AbortableOpts = {
  boxPrimitives?: boolean;
  noRecursive?: boolean;
};

abortable<T>(obj: T, signal: AbortSignal, opts?: AbortableOpts): T

Example

import { abortable } from 'nfkit';

const ac = new AbortController();
const obj = abortable({ n: 1 }, ac.signal);

obj.n; // 1
ac.abort('stop');
// later access throws AbortedError
6) middleware-dispatcher

Generic middleware execution model: (args..., next).

Includes 3 dispatchers:

  • DynamicMiddlewareDispatcher<F>: override buildMiddlewares
  • MiddlewareDispatcher<F>: direct array registration
  • ProtoMiddlewareDispatcher<A>: middleware by instance prototype chain

Core types/options

type Middleware<F>
type MiddlewareNext<F>
type MiddlewareDispatcherOptions<F> = {
  acceptResult?: (res) => boolean | Promise<boolean>;
  errorHandler?: (e, args, next) => any;
};

Example

import { MiddlewareDispatcher } from 'nfkit';

type Handler = (x: number) => number;
const d = new MiddlewareDispatcher<Handler>();

d.middleware(async (x, next) => {
  const r = await next();
  return (r ?? 0) + 1;
});

d.middleware(async (x) => x * 10);

const out = await d.dispatch(2); // 21
7) i18n

Middleware-based translation pipeline with built-in dictionary lookup middleware.

API

class I18n<Ex extends any[] = []> {
  constructor(options: { locales: string[]; defaultLocale?: string });
  middleware(mw: I18nMiddleware<Ex>, prior?: boolean): this;
  removeMiddleware(mw: I18nMiddleware<Ex>): this;
  getExactLocale(locale: string): string;
  translateString(locale: string, text: string, ...ex: Ex): Promise<string>;
  translate<T>(locale: string, obj: T, ...ex: Ex): Promise<T>;
}

type I18nDictionary = Record<string, Record<string, string>>;
I18nLookupMiddleware(
  dictOrFactory,
  options?: { matchType?: 'exact' | 'hierarchy' | 'startsWith' },
)
createI18nLookupMiddleware<Ex>()

Example

import { I18n, I18nLookupMiddleware } from 'nfkit';

const i18n = new I18n({ locales: ['en', 'zh', 'zh-Hans'], defaultLocale: 'en' });

i18n.middleware(
  I18nLookupMiddleware(
    {
      en: { hello: 'Hello' },
      'zh-Hans': { hello: '你好' },
    },
    { matchType: 'hierarchy' },
  ),
);

const s = await i18n.translateString('zh-Hans-CN', 'Say #{hello}');
// Say 你好
8) patch-string-in-object

Deeply traverses an object graph and transforms all string values (async callback supported).

API

patchStringInObject<T>(
  obj: T,
  cb: (s: string) => string | Promise<string>,
): Promise<T>

Example

import { patchStringInObject } from 'nfkit';

const out = await patchStringInObject(
  { a: 'hello', nested: ['x', 'y'] },
  (s) => s.toUpperCase(),
);
// { a: 'HELLO', nested: ['X', 'Y'] }
9) observe-diff

Creates a proxy that reports add/update/delete changes on property writes/deletes.

API

observeDiff<T>(
  obj: T,
  cb: (change: {
    type: 'add' | 'update' | 'delete';
    key: keyof T;
    oldValue: T[keyof T] | undefined;
    newValue: T[keyof T] | undefined;
  }) => any,
): T

Example

import { observeDiff } from 'nfkit';

const state = observeDiff<{ count?: number }>({}, (c) => {
  console.log(c.type, c.key, c.oldValue, c.newValue);
});

state.count = 1; // add
state.count = 2; // update
delete state.count; // delete
10) memorize

Per-instance memoization decorator for methods/getters.

API

Memorize(): MethodDecorator & PropertyDecorator

Example

import { Memorize } from 'nfkit';

class Svc {
  calls = 0;

  @Memorize()
  expensive() {
    this.calls += 1;
    return Math.random();
  }
}

Requires TypeScript decorator support (for example experimentalDecorators).

11) may-be-array

Utility for handling single value or array input consistently.

API

type MayBeArray<T> = T | T[];
makeArray<T>(value: MayBeArray<T>): T[];

Example

import { makeArray } from 'nfkit';

makeArray(1); // [1]
makeArray([1, 2]); // [1, 2]
12) app-context

Lightweight app context / DI-like container.

Supports:

  • provider registration via provide
  • context composition via use
  • lifecycle initialization via start
  • service lookup via get/getAsync

Core API

createAppContext<Req = Empty>(): AppContext<Empty, Req>

ctx.provide(ServiceClass, ...args?, options?)
ctx.use(otherCtx)
ctx.get(ServiceClass)
ctx.getAsync(ServiceClass)
ctx.define()
ctx.start()

Type-driven IoC contract

provide() is type constrained by AppServiceClass, whose constructor signature is:

new (ctx: AppContext<...>, ...args) => Service

That means the first constructor parameter must be ctx.

The type system also tracks context shape changes:

  • provide: 'name' adds ctx.name as that service instance type.
  • merge: ['memberA', 'memberB'] maps selected members from service onto ctx with correct member types.
  • use(otherCtx) merges provided/required context types across contexts.
  • start() resolves to never at type level if required context (Req) is not satisfied.

Common provide options:

{
  provide?: string; // expose service instance as ctx[prop]
  merge?: string[]; // map selected service members onto ctx
  useValue?: instance | Promise<instance>;
  useFactory?: (ctx, ...args) => instance | Promise<instance>;
  useClass?: Class;
}

provide vs merge:

  • provide: exposes the whole service instance on context (for example ctx.logger).
  • merge: exposes selected service members directly on context (for example ctx.info from ctx.logger.info).

Current runtime semantics:

  • ctx.provide/ctx.use inside a provider constructor: newly added providers are settled after that constructor ends.
  • ctx.provide/ctx.use inside init(): newly added providers are settled and initialized immediately after the current init ends.
  • calling provide/use after start(): providers are queued; call define() to trigger settlement.
  • use(startedCtx): merges existing instances directly; no re-construction and no re-init.

Example

import { AppContext, createAppContext } from 'nfkit';

class LoggerService {
  constructor(
    public ctx: AppContext,
    private prefix = '[app]',
  ) {}

  logs: string[] = [];

  info(s: string) {
    this.logs.push(`${this.prefix} ${s}`);
  }
}

const app = await createAppContext()
  // first constructor argument of provider class is always ctx
  .provide(LoggerService, '[core]', {
    provide: 'logger', // ctx.logger -> LoggerService
    merge: ['info'], // ctx.info -> bound logger.info
  })
  .define()
  .start();

app.logger.info('from instance');
app.info('from merged method');
13) configurer

Config helper for string-based configuration (env-style), with typed getters.

API

class Configurer<T extends Record<string, string>> {
  constructor(defaultConfig: T);
  loadConfig(options?: {
    env?: Record<string, string | undefined>;
    obj?: any;
  }): ConfigurerInstance<T>;
  generateExampleObject(): Record<string, unknown>;
}

class ConfigurerInstance<T extends Record<string, string>> {
  getString(key): string;
  getInt(key): number;
  getFloat(key): number;
  getBoolean(key): boolean;
  getStringArray(key): string[];
  getIntArray(key): number[];
  getFloatArray(key): number[];
  getBooleanArray(key): boolean[];
  getJSON<R>(key): R;
}

Merge priority:

defaultConfig < obj < env

Example

import { Configurer } from 'nfkit';

const cfg = new Configurer({ PORT: '3000', ENABLE_CACHE: '1' }).loadConfig({
  env: { PORT: '8080' },
});

cfg.getInt('PORT'); // 8080
cfg.getBoolean('ENABLE_CACHE'); // true
14) types

Shared type exports:

type Awaitable<T> = T | Promise<T>;
type AnyClass = new (...args: any[]) => any;
type ClassType<T = any> = new (...args: any[]) => T;
interface Empty {}
type Prettify<T> = ...

Notes

This README reflects the current implementation exported by index.ts. If exports change in future versions, treat source/types as the final reference.