nfkit
v1.0.37
Published
Common kits
Readme
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 | T[] type helper + normalization utility |
| 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 nfkitQuick 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 === 22) 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(): numberExample
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(); // a5) 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): TExample
import { abortable } from 'nfkit';
const ac = new AbortController();
const obj = abortable({ n: 1 }, ac.signal);
obj.n; // 1
ac.abort('stop');
// later access throws AbortedError6) middleware-dispatcher
Generic middleware execution model: (args..., next).
Includes 3 dispatchers:
DynamicMiddlewareDispatcher<F>: overridebuildMiddlewaresMiddlewareDispatcher<F>: direct array registrationProtoMiddlewareDispatcher<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); // 217) 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,
): TExample
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; // delete10) memorize
Per-instance memoization decorator for methods/getters.
API
Memorize(): MethodDecorator & PropertyDecoratorExample
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) => ServiceThat means the first constructor parameter must be ctx.
The type system also tracks context shape changes:
provide: 'name'addsctx.nameas that service instance type.merge: ['memberA', 'memberB']maps selected members from service ontoctxwith correct member types.use(otherCtx)merges provided/required context types across contexts.start()resolves toneverat 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 examplectx.logger).merge: exposes selected service members directly on context (for examplectx.infofromctx.logger.info).
Current runtime semantics:
ctx.provide/ctx.useinside a provider constructor: newly added providers are settled after that constructor ends.ctx.provide/ctx.useinsideinit(): newly added providers are settled and initialized immediately after the currentinitends.- calling
provide/useafterstart(): providers are queued; calldefine()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'); // true14) 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.
