Migrating to 2.3
2.3.0 retains Node 20+, TypeScript 5.0+, TC39 decorators, ESM/CJS subpaths and
zero runtime dependencies. A stable package release does not automatically
promote experimental APIs. Integration checks do not certify remote providers
or distributed coordination.
API status
Section titled “API status”| Feature | Status in 2.3 | Import |
|---|---|---|
rateLimit, memoize({ maxEntries }) |
Stable | /operators |
executionMethod |
Stable | /run |
TransitionTo(..., { concurrency: 'reject-stale' }) |
Stable, opt-in | Root or /behavioral/state-machine |
| Breaker, keyed lock and semaphore handles | Stable | /operators |
| Event adapters | Stable | /observability |
cancelWhenUnused |
Experimental, opt-in | Cache/overlap options |
observeOperation, asyncCachedFor, CAS |
Experimental | /experimental |
New options are disabled by default. Existing operators retain synchronous results and their previous promise behavior without a signal when shared cancellation is disabled. Public event, error-code and policy-layer unions are unchanged.
Admission and capacity
Section titled “Admission and capacity”import { memoize, rateLimit } from 'annotations-design-patterns/operators';import { pipe } from 'annotations-design-patterns/pipe';
const cachedLength = pipe( (value: string) => value.length, memoize<object, readonly [string], number>({ maxEntries: 100 }),);
const admitted = pipe(() => 'ok', rateLimit({ limit: 20, windowMs: 1_000 }));rateLimit counts admissions in a monotonic sliding window per applied method.
Excess calls return a rejected promise with RateLimitExceededError and
retryAfterMs; an admitted synchronous call retains its synchronous result.
Failures consume quota. A pre-aborted signal does not. The existing
@RateLimit decorator is unchanged.
maxEntries must be a positive safe integer. Entries are FIFO, including pending
results; hits do not reorder them. With perReceiver, capacity applies per
receiver. Eviction does not stop the underlying execution.
Effective signal and the last consumer
Section titled “Effective signal and the last consumer”import { executionMethod, run } from 'annotations-design-patterns/run';import { memoize } from 'annotations-design-patterns/operators';import { pipe } from 'annotations-design-patterns/pipe';
const load = pipe( executionMethod(async ({ signal }, id: string) => { const response = await fetch(`/records/${encodeURIComponent(id)}`, { signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.text(); }), memoize<object, readonly [string], string>({ key: (id) => id, maxEntries: 100, cancelWhenUnused: true, }),);
const caller = new AbortController();const result = run({}, ['record-1'], load, { signal: caller.signal });executionMethod receives the effective signal derived by operators. Direct
calls have no contextual signal. Use pipe/run: .apply() and opaque wrappers
are explicit context boundaries; there is no ambient propagation.
cancelWhenUnused is available on memoize, cachedFor and overlap in share
mode. Each signalled consumer can withdraw from its wait. An unsignalled consumer
remains until settlement. The last withdrawal retires the visible reservation
before aborting the shared root: reentrant calls can create a replacement, and
late settlement cannot remove it. The backend stops only if it cooperates.
Enabling this option does not preserve previous promise identity as a contract. A synchronous initiator remains synchronous; a reentrant consumer of a pending reservation receives a promise. Eviction, TTL and invalidation do not withdraw consumers. Cyclic waits on an execution’s own reservation are unsupported.
StateMachine: first valid commit
Section titled “StateMachine: first valid commit”import { StateMachine, TransitionTo, When } from 'annotations-design-patterns';
class Document extends StateMachine<'draft' | 'published'> { @When('draft') @TransitionTo('published', { concurrency: 'reject-stale' }) async publish(): Promise<void> { // Persist effects that can tolerate a subsequent transition conflict. }}Manual, A-B-A and same-state transitions invalidate pending work. Its completion
rejects with StateTransitionConflictError; external effects are not rolled back
and no retry is automatic. Optimistic commit is internal and does not invoke a
virtual transitionTo override. Overrides used for manual changes must delegate
to super.transitionTo. Without the option, virtual dispatch is preserved.
Handles and observation
Section titled “Handles and observation”circuitBreakerWithHandle, keyedLockWithHandle and semaphoreWithHandle
return { operator, handle }. Applications of that factory’s operator share its
state; ordinary operators retain per-application state. inspect returns
immutable snapshots. Breaker reset advances its generation so older work and
probes cannot mutate the new state. It neither aborts work nor revokes permits.
Lock and semaphore handles have no reset.
createCounterSink, createSpanEventSink and combineEventSinks isolate callback
throws and accidental rejections. Unknown operators use the custom label;
operation IDs and arguments are excluded from labels.
Sinks passed directly to run also isolate accidentally returned asynchronous
rejections. emit(): void still does not await the callback or add backpressure;
the business result retains its synchronous or asynchronous channel.
observeOperation measures the result visible at its position in a composition,
not physical backend completion. Cancellation provenance distinguishes
cancelled from timeout or an error merely named AbortError. A pre-abort that
run rejects before entering the layer produces no observation. Invocation data
requires includeInvocation: true.
External cache and CAS: experimental contracts
Section titled “External cache and CAS: experimental contracts”asyncCachedFor is always asynchronous. It requires a provider declaring real
TTL support, namespace, schema version, explicit key, synchronous codec, finite
I/O deadline and storeFailure policy. bypass tolerates all get/set failures,
including permissions/configuration; key, envelope and codec errors reject.
onDiagnostic reports provider failures without changing the business result.
A deadline bounds waiting; an already-started read or write can complete later. No new business work or write starts after cancellation. There is no distributed miss deduplication or protection from stale late writes.
VersionedMemoryStore is a local CAS reference. Tokens belong to their store,
are never reused within an instance, and distinguish absence from an undefined
value. Absent versions can be invalidated by writes to other keys, causing
conservative conflicts. The fallback without Web Crypto cannot guarantee
cross-instance uniqueness. This does not certify distributed locks, breakers
or semaphores; those require specific providers and network-failure testing.
Snapshots freeze only their structure. Values are stored and returned by reference, without cloning or deep freezing; treat them as immutable. External mutation of an object does not advance its CAS version.
AsyncMemoryStore timing change
Section titled “AsyncMemoryStore timing change”In 2.2, set, delete or clear could overtake a pending update and be
overwritten when it completed. Mutations now follow admission order per key;
clear and cleanup are mutation barriers. get/has preserve non-blocking
reads of the last committed value, including reads inside an updater.
Before adopting 2.3, review updaters that await another mutation of their own key or a barrier: they would create a circular dependency. Return the new value from the updater instead of writing it inside. The queue has no backpressure, and TTL starts when the write commits.
Size and release preparation
Section titled “Size and release preparation”The minified browser consumer using memoize increases from 1,664 to 2,800 gzip
bytes in local 2.2 versus 2.3 runtime measurements. Its existing factory retains the
optional sharing path. This is neither a Bundlephobia result nor the size of
the whole package. Its budget was explicitly reviewed; other consumer limits
were retained.
Stable publication uses a version tag and the release workflow: the exact tarball is validated before publication with npm provenance. Integration and CI results do not promote experimental APIs to stable status.

