# 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

```bash
npm i nfkit
```

## Quick Start

```ts
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**

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

**Example**

```ts
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**

```ts
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**

```ts
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**

```ts
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**

```ts
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**

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

**Example**

```ts
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**

```ts
class AbortedError extends Error {}

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

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

**Example**

```ts
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**

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

**Example**

```ts
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**

```ts
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**

```ts
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**

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

**Example**

```ts
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**

```ts
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**

```ts
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**

```ts
Memorize(): MethodDecorator & PropertyDecorator
```

**Example**

```ts
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**

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

**Example**

```ts
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**

```ts
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:

```ts
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:

```ts
{
  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**

```ts
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**

```ts
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**

```ts
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:

```ts
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.
