@h1ylabs/promise-aop
v0.6.0
Published
**Latest version: v0.6.0**
Readme
Promise-AOP
Latest version: v0.6.0
A TypeScript-first AOP (Aspect-Oriented Programming) framework designed as a core infrastructure component for building robust asynchronous applications. Promise-AOP provides a type-safe, zero-dependency foundation for managing cross-cutting concerns with explicit context management and thread-safe execution.
Note: This is a core library intended for use as a building block in other frameworks and applications. The API prioritizes technical precision and extensibility over simplicity.
Design Principles
Promise-AOP is built on the following technical foundations:
1. Type Safety First
- Full TypeScript support with generic type inference
- Context access restricted by declared sections at compile-time
- No runtime type coercion or dynamic property access
2. Explicit Context Management
- Section-based context system prevents concurrent access conflicts
- Thread-safe execution using Node.js
AsyncLocalStorage - Dependency graph resolution for advice ordering
3. Composable Architecture
- Aspects as first-class, reusable units
- Process compilation separates configuration from execution
- Advice chaining with configurable execution strategies
4. Zero Dependencies
- Built entirely on Node.js standard library
- No external runtime dependencies
- Minimal surface area for security and compatibility
Architecture Overview
Promise-AOP implements a three-layer execution model:
flowchart TD
A["Aspect Definition<br/>(Cross-cutting Concern)"]
B["Process Compilation<br/>(Dependency Resolution)"]
C["Runtime Execution<br/>(AsyncContext + Advice Chain)"]
A --> B
B --> C
subgraph "Layer 1: Definition"
D["createAspect()"]
E["Advice Metadata"]
F["Section Dependencies"]
end
subgraph "Layer 2: Compilation"
G["organizeAspect()"]
H["Dependency Graph"]
I["Execution Plan"]
end
subgraph "Layer 3: Runtime"
J["AsyncLocalStorage"]
K["Section Locking"]
L["Error Propagation"]
end
A -.-> D
B -.-> G
C -.-> JExecution Flow
The framework executes advice in a well-defined order with configurable error handling:
sequenceDiagram
participant Context
participant Before
participant Around
participant Target
participant AfterReturning
participant AfterThrowing
participant After
Context->>Before: Execute (parallel by default)
Before->>Around: Setup wrappers (sequential)
Around->>Target: Execute wrapped target
alt Success
Target->>AfterReturning: Pass result
AfterReturning->>After: Continue
else Error
Target->>AfterThrowing: Pass error
AfterThrowing->>After: Continue
end
After->>Context: Complete (always runs)Key Execution Properties:
beforeadvice executes in parallel unless dependencies specifiedaroundadvice wraps target with two composition points (target/result)- Error handling uses specialized
Rejectiontypes for control flow - Context access is protected by section-based locking mechanism
Installation
# npm
npm install @h1ylabs/promise-aop
# yarn
yarn add @h1ylabs/promise-aop
# pnpm
pnpm add @h1ylabs/promise-aopRequirements: Node.js 16+ (uses AsyncLocalStorage)
Core Concepts
Aspect
A modular unit encapsulating a cross-cutting concern with typed advice:
type Aspect<Result, Context> = {
readonly name: string;
readonly before?: AdviceMetadata<Result, Context, "before">;
readonly around?: AdviceMetadata<Result, Context, "around">;
readonly afterReturning?: AdviceMetadata<Result, Context, "afterReturning">;
readonly afterThrowing?: AdviceMetadata<Result, Context, "afterThrowing">;
readonly after?: AdviceMetadata<Result, Context, "after">;
};Process
Compiled execution chain with resolved dependencies and error handling configuration:
type Process<Result, Context> = (
context: ContextAccessor<Context>,
exit: ExecutionOuterContext,
target: Target<Result>,
) => Promise<Result>;Context & Sections
Thread-safe shared state accessed through declared sections:
// Context is split into sections for fine-grained access control
type AppContext = {
logger: Logger;
db: Database;
cache: Cache;
};
// Advice declares which sections it needs
const aspect = createAspect<Data, AppContext>((createAdvice) => ({
name: "example",
before: createAdvice({
use: ["logger", "db"], // Type-checked access
advice: async ({ logger, db }) => {
// Only logger and db are accessible
// cache is not available (compile error if accessed)
},
}),
}));Quick Start
import { createAspect, createProcess, runProcess } from "@h1ylabs/promise-aop";
// 1. Define context type
type AppContext = { logger: Console };
// 2. Create aspect
const LoggingAspect = createAspect<string, AppContext>((createAdvice) => ({
name: "logging",
before: createAdvice({
use: ["logger"],
advice: async ({ logger }) => logger.info("Start"),
}),
after: createAdvice({
use: ["logger"],
advice: async ({ logger }) => logger.info("Complete"),
}),
}));
// 3. Compile process
const process = createProcess<string, AppContext>({
aspects: [LoggingAspect],
});
// 4. Execute
const result = await runProcess({
process,
context: () => ({ logger: console }),
target: async () => "Hello, AOP!",
});API Reference
Core Functions
createAspect<Result, Context>(factory)
Creates an aspect with typed advice definitions.
Parameters:
factory:(createAdvice: AdviceHelper) => Aspect<Result, Context>
Returns: Aspect<Result, Context>
Type Parameters:
Result: Return type of target functionsContext: Shared context object (section-based)
createProcess<Result, Context>(config)
Compiles aspects into an executable process with dependency resolution.
Parameters:
{
aspects: readonly Aspect<Result, Context>[];
buildOptions?: BuildOptions;
processOptions?: ProcessOptions<Result, Context>;
}Returns: Process<Result, Context>
Compilation Steps:
- Collect advice by type from all aspects
- Resolve dependencies using topological sort
- Detect circular dependencies and section conflicts
- Generate execution plan based on
BuildOptions
runProcess<Result, Context>(props)
Executes a process with target function and context.
Parameters:
{
process: Process<Result, Context>;
target: Target<Result>;
context: ContextGenerator<Context> | AsyncContext<Context>;
}Returns: Promise<Result>
runProcessWith<Result, Context>(props)
Advanced execution with explicit AsyncContext instance for reuse.
Parameters:
{
process: Process<Result, Context>;
target: Target<Result>;
context: AsyncContext<Context>; // Pre-created instance
contextGenerator: ContextGenerator<Context>;
}Use Case: Optimize performance by reusing AsyncContext across multiple executions.
Advice Types & Signatures
| Advice Type | Signature | Execution Timing |
| ---------------- | ------------------------------------------------------------------------------- | ----------------------- |
| before | (context: Restricted<Context, Sections>) => Promise<void> | Before target execution |
| around | (context: Restricted<Context, Sections>, hooks: AroundHooks) => Promise<void> | Wraps target execution |
| afterReturning | (context: Restricted<Context, Sections>, result: Result) => Promise<void> | After successful return |
| afterThrowing | (context: Restricted<Context, Sections>, error: unknown) => Promise<void> | After error thrown |
| after | (context: Restricted<Context, Sections>) => Promise<void> | Always (finally block) |
BuildOptions
Controls execution strategy and error handling per advice type:
type BuildOptions = {
advice?: {
[AdviceType in Advice]?: {
execution?: "parallel" | "sequential";
error?: {
aggregation?: "unit" | "all"; // Stop on first or collect all
runtime?: {
afterThrow?: "halt" | "continue"; // Propagate or continue
};
};
};
};
};Default Configuration:
| Advice Type | Execution | Aggregation | After Throw | Rationale |
| ---------------- | ------------ | ----------- | ----------- | ----------------------------------- |
| before | parallel | unit | halt | Fail fast on validation errors |
| around | sequential | unit | halt | Preserve wrapper order |
| afterReturning | parallel | all | continue | Best-effort cleanup |
| afterThrowing | parallel | all | continue | Collect all error handling failures |
| after | parallel | all | continue | Finally block semantics |
ProcessOptions
Centralized error handling with three extension points:
type ProcessOptions<Result, Context> = {
// Select primary error from multiple failures
determineError?: (props: {
context: ContextAccessor<Context>;
exit: ExecutionOuterContext;
errors: unknown[];
info: ErrorInfo;
}) => Promise<unknown>;
// Implement recovery logic
handleError?: (props: {
currentTarget: Target<Result>;
context: ContextAccessor<Context>;
exit: ExecutionOuterContext;
error: unknown;
}) => Promise<Result>;
// Handle continued errors (non-halting)
handleContinuedErrors?: (props: {
context: ContextAccessor<Context>;
exit: ExecutionOuterContext;
errors: readonly (readonly [unknown[], ErrorInfo])[];
}) => Promise<void>;
};Error Flow:
flowchart LR
A[Errors] --> B[determineError]
B --> C[Primary Error]
C --> D[handleError]
D --> E{Recovery}
E -->|Success| F[Result]
E -->|Failure| G[Throw]
A --> H[handleContinuedErrors]
H --> I[Logging/Metrics]Advanced Types
Context Types
// Context generator (simple function)
type ContextGenerator<Context> = () => Context;
// Context accessor (read-only access)
type ContextAccessor<Context> = () => Context;
// Section-based restriction
type Restricted<Context, Sections> =
Pick<Context, Sections[number]> & Readonly<...>;
// Sections declaration
type SectionsUsed<Context> = readonly (keyof Context)[];Target & Wrapper Types
// Async target function
type Target<Result> = () => Promise<Result>;
// Target wrapper (for around advice)
type TargetWrapper<Result> = (target: Target<Result>) => Target<Result>;
// Around advice resolver
type AroundAdviceResolver<Result> = (
propagateChain: TargetWrapper<Result>,
nextChain: TargetWrapper<Result>,
) => Target<Result>;Error Types
Promise-AOP uses specialized Rejection types for fine-grained error control flow:
// Base rejection type
class Rejection extends Error {
constructor(
public readonly errors: unknown[],
public readonly info: ErrorInfo,
) {}
}
// Halting rejection (stops execution)
class HaltRejection extends Rejection {}
// Continuous rejection (collected, non-halting)
class ContinuousRejection extends Rejection {}
// Error context information
type ErrorInfo =
| { occurredFrom: "target" }
| { occurredFrom: "unknown" }
| { occurredFrom: "advice"; advice: Advice };Rejection Type Usage:
Rejection: Base class thrown by advice when errors occur. Contains array of errors and context information about where the error originated.HaltRejection: Created when an error should stop execution immediately. Used by advice types withafterThrow: "halt"configuration. TriggershandleErrorinProcessOptions.ContinuousRejection: Created when an error should be collected but not halt execution. Used by advice types withafterThrow: "continue"configuration. Collected and passed tohandleContinuedErrors.
Error Origin Tracking:
occurredFrom: "target": Error thrown by the target function itselfoccurredFrom: "advice": Error thrown during advice execution (includes which advice type)occurredFrom: "unknown": Error from unexpected sources (framework internal)
Advanced Implementation
Internal Execution Model
Promise-AOP uses a multi-phase execution model with explicit error boundaries:
Compilation Phase (
createProcess):- Aspects are organized by advice type
- Dependency graphs are built using topological sort
- Section conflicts are detected at compilation time
- Execution strategies are frozen into the process
Setup Phase (
runProcess):- AsyncContext is initialized with context generator
- Context sections are locked based on advice declarations
- Error handlers are attached to execution chain
Execution Phase:
beforeadvice executes with configurable parallelismaroundadvice builds wrapper chains (target then result)- Target executes within composed wrappers
afterReturningorafterThrowingbased on outcomeafteralways executes (finally semantics)
Teardown Phase:
- Continuous rejections are collected
- Halt rejection triggers error handlers
- AsyncContext is cleaned up
Around Advice Composition
The around advice provides two composition points for maximum flexibility:
const aspect = createAspect<number, Context>((createAdvice) => ({
name: "composition",
around: createAdvice({
use: ["logger"],
advice: async ({ logger }, { attachToTarget, attachToResult }) => {
// attachToTarget: wraps the original target function
// Executes closest to the target
attachToTarget((target) => async () => {
logger.info("Target wrapper: before");
const result = await target();
logger.info("Target wrapper: after");
return result + 1;
});
// attachToResult: wraps the entire execution chain
// Executes at the outermost layer
attachToResult((target) => async () => {
logger.info("Result wrapper: before");
const result = await target();
logger.info("Result wrapper: after");
return result * 2;
});
},
}),
}));
// Execution order (LIFO):
// Result wrapper: before
// Target wrapper: before
// [original target]
// Target wrapper: after
// Result wrapper: after
// Final: (originalResult + 1) * 2Composition Visualization:
graph LR
A[Result Wrapper] --> B[Next Chain]
B --> C[Target Wrapper] --> D[Original Target]
D --> E[Return + 1]
E --> F[Return * 2]
F --> G[Final Result]Optimization Guidelines
Process Reuse: Create process once, reuse across executions
// Good const process = createProcess({ aspects }); const execute = (target) => runProcess({ process, target, context }); // Bad const execute = (target) => runProcess({ process: createProcess({ aspects }), target, context });Section Granularity: Use fine-grained sections for better parallelism
// Good: separate sections type Context = { readOnlyConfig: Config; logger: Logger; metrics: Metrics; }; // Bad: coarse sections type Context = { services: { logger: Logger; metrics: Metrics; config: Config }; };AsyncContext Reuse: Use
runProcessWithfor high-frequency executionsclass ServiceManager { private asyncContext = AsyncContext.create<Context>(); execute<T>(target: Target<T>) { return runProcessWith({ process: this.process, target, context: this.asyncContext, // Reused contextGenerator: () => this.buildContext(), }); } }
Extension Guide
Promise-AOP is designed to be extended for domain-specific frameworks:
Building a Framework
// 1. Define domain-specific context
type DomainContext = {
auth: AuthService;
db: DatabaseConnection;
logger: Logger;
};
// 2. Create reusable aspect library
export const AuthAspect = createAspect<any, DomainContext>((createAdvice) => ({
name: "auth",
before: createAdvice({
use: ["auth"],
advice: async ({ auth }) => {
if (!auth.isAuthenticated()) {
throw new UnauthorizedError();
}
},
}),
}));
// 3. Provide high-level API
export class DomainService {
private process = createProcess<any, DomainContext>({
aspects: [AuthAspect, LoggingAspect, MetricsAspect],
});
async execute<T>(target: Target<T>): Promise<T> {
return runProcess({
process: this.process,
target,
context: () => this.buildContext(),
});
}
}Custom Advice Types
While the five advice types cover most use cases, you can simulate custom timing:
// Simulated "beforeValidation" using dependsOn
const ValidationAspect = createAspect(/* ... */);
const BeforeValidationAspect = createAspect((createAdvice) => ({
name: "beforeValidation",
before: createAdvice({
dependsOn: ["validation"], // Runs before validation
advice: async () => {
/* ... */
},
}),
}));Integration with Other Systems
// Example: OpenTelemetry integration
const TracingAspect = createAspect<any, { tracer: Tracer }>((createAdvice) => ({
name: "tracing",
around: createAdvice({
use: ["tracer"],
advice: async ({ tracer }, { attachToTarget }) => {
attachToTarget((target) => async () => {
const span = tracer.startSpan("operation");
try {
return await target();
} finally {
span.end();
}
});
},
}),
}));Design Constraints & Trade-offs
No Dynamic Aspect Composition: Aspects must be known at process compilation time for performance and type safety
Section-Based Locking: Prevents data races but requires upfront section declaration
AsyncLocalStorage Dependency: Requires Node.js 16+, not suitable for browsers
Synchronous Advice Not Supported: All advice must return
Promise<void>for consistent async handlingProcess Immutability: Compiled processes are immutable for thread-safety and memoization
Development
# Install dependencies
yarn install
# Run tests
yarn test
# Build the library
yarn build
# Type check
yarn check-types
# Lint
yarn lintLicense
MIT © h1ylabs
