Skip to content

Migrating to 2.2

2.2.0 extends the 2.1 correctness foundation with explicit options and a functional operator. It is the published reference for migration to 2.3. Node 20+, strict TypeScript, TC39 decorators, ESM/CJS, subpaths, tree-shaking and zero runtime dependencies remain supported.

import { semaphore } from 'annotations-design-patterns/operators';
import { pipe } from 'annotations-design-patterns/pipe';
const limited = pipe<object, readonly [tenantId: string], string>(
async (tenantId) => tenantId,
semaphore({ permits: 4, key: ({ args }) => args[0], maxQueue: 100 }),
);
await limited.call({}, 'tenant-a');

semaphore always returns a Promise. Admitted work starts synchronously and its permit is released when inner execution settles through resolution, rejection, or throw. Permits are positive safe integers; configured maxQueue is a non-negative safe integer. The default queue has no configured bound.

Without a key, one applied pipeline shares a pool across receivers. A key partitions pools; its resolver receives the readonly invocation-key context used by overlap and keyedLock. An explicit run() key overrides the resolver.

Admission is FIFO without barging. Pre-aborted signals prevent execution; aborted waiters withdraw. Aborting an active holder rejects its caller, while non-cooperative work retains the permit until settlement. A full queue rejects with SemaphoreQueueFullError, using the existing POLICY_ERROR code and an optional key identifying its partition.

@Semaphore(n) retains per-instance pools, Promise failures and eager execution; it now adapts that same operator and PermitQueue. Existing semaphore.queued, semaphore.acquired and semaphore.released kinds are reused without extending PolicyEvent.

import { cachedFor, memoize } from 'annotations-design-patterns/operators';
const memoized = memoize<object, readonly [id: string], string>({
perReceiver: true,
}).apply((id) => id);
const cached = cachedFor<object, readonly [id: string], string>(60_000, {
perReceiver: true,
maxEntries: 1_000,
}).apply((id) => id);

With perReceiver true, distinct objects or functions have separate caches; the same receiver and arguments share an entry. Identity is stored in a WeakMap. Undefined, null and all primitive receivers, including symbols, produce an explicit TypeError. A defined perReceiver option must be boolean.

Omitting the option preserves functional pipeline-wide caches across receivers. Decorators remain isolated by instance.

memoizeWithHandle and cachedForWithHandle omit perReceiver from object-literal options and reject enabling it at runtime. A structurally compatible options variable may compile, but cannot enable a mode without an invalidation contract. Handles still provide only invalidate(key: string): void and clear(): void; there is no size property or weak-partition enumeration.

New perReceiver/maxEntries fields are captured at factory creation. Mutating the original object later cannot change partitions or capacity. Existing custom key functions retain capture at operator apply.

maxEntries is a positive safe integer; explicit Infinity, zero and fractions are invalid. Omission preserves unlimited capacity. The bound applies per receiver partition or per handle shared across applications of the same operator.

Eviction follows insertion order: hits do not refresh an entry’s position. Pending promises count; eviction does not cancel their existing consumers. A stale rejection cannot delete a newer entry for the same key.

Only bounded caches prune expired entries lazily on misses. TTL cleanup is silent; capacity eviction emits existing cache.invalidated with the evicted key. The bound is restored before notifying sinks, including reentrant sinks. No permanent timers, global sweeper or LRU are introduced.

TTL still starts at invocation, including pending promises. Infinity means no expiration; functional cachedFor(0) still disables reuse.

The default remains JSON.stringify(args); existing cache identities are not changed and no universal serializer is introduced.

Input Default limitation
undefined, null, NaN, infinities, Symbols or functions as arguments Can become [null] and collide
-0 and 0 Same representation
undefined/Symbol/function properties and Symbol keys Omitted; can collide with an empty object
Objects Property insertion order can distinguish keys; this is not reference identity
Arrays Order is preserved; holes, undefined and null can collide
Date Uses ISO and can collide with an equivalent string; invalid dates become null
Map and Set Typically serialize like {} without own serializable properties
BigInt or cyclic objects Throw TypeError before the method executes
Custom toJSON or getters Execute and can change a key or throw
Receiver Excluded from the key; use perReceiver to separate identities

Use key: (...args) => string for domain keys, including BigInt, cycles and identity-sensitive inputs. The existing @MemoizeByRef decorator offers distinct reference semantics; it does not change the functional default.

The synchronous isFailure(error, info) predicate is available through the operator, @CircuitBreaker and definePolicy. Readonly info contains receiver, a readonly argument copy, key, operationId and metadata, without signal, sink or execution controls. This is shallow protection: the receiver and nested objects remain application-owned references.

True counts a failure. False ignores it without incrementing or resetting failures and without treating it as success. The original error still rejects. In HALF_OPEN, ignored errors release the current probe, preserve OPEN history and do not close the circuit. A throwing predicate rejects with its decision error without counting the original error, as shouldRetry does. Caller cancellation and stale generations are handled before invoking the predicate.

maxDelayMs accepts finite values from 0 through 2_147_483_647, including fractions. The technical timer cap remains mandatory even without this option.

Order: base delay → exponential backoff → minimum of technical cap and configured maxDelayMs → jitter.

jitter Wait derived from capped delay cap
none, default cap; does not call Math.random
full random × cap
equal cap / 2 + random × cap / 2

A zero result uses the cancellable microtask retry path. No public random source is added. @Retry and definePolicy share these options; describe includes new maxDelayMs/jitter fields only when explicitly configured.

Observer event maps can now be named interfaces while preserving On/Emits event-payload relationships and the default unknown-payload Subject escape hatch. Existing closed-union When/TransitionTo checks remain unchanged.

Instance-scoped decorators provide explicit diagnostics for unbound calls and primitive receivers, including symbols. Valid objects/functions and each API’s existing synchronous or Promise error channel remain supported.

Shared cancellation remains isolated per consumer: all consumers abandoning work does not automatically abort it. Functional rateLimit, new async StateMachine transition policies, distributed stores/locks, vendor adapters and global testing registries remain outside runtime. Repository POST-2.1-RFCS.md records designs and decisions, not published APIs.

The observability guide uses actual exports/events and shows external callback recipes. No tracing or metrics SDK enters the package.

npm run bench measures ten paths with warmup and multiple samples: direct call, operator, decorator, pipe, policy, memoize hit/miss, retry success, breaker CLOSED and uncontended semaphore. Compare two runs manually:

Ventana de terminal
npm run bench -- --output bench-baseline.json
npm run bench -- --compare bench-baseline.json --max-ratio 3 --output bench-current.json

Comparison requires identical Node version, platform, architecture and sampling parameters. The 3× threshold targets large regressions rather than replacing production measurements. Percentiles describe sample means, not individual-call latency. npm run bundle:check maintains reproducible size budgets; changes require reviewed measurements rather than automatic rebasing.