npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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.

한국어 문서 (Korean Documentation)


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 -.-> J

Execution 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:

  • before advice executes in parallel unless dependencies specified
  • around advice wraps target with two composition points (target/result)
  • Error handling uses specialized Rejection types 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-aop

Requirements: 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 functions
  • Context: 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:

  1. Collect advice by type from all aspects
  2. Resolve dependencies using topological sort
  3. Detect circular dependencies and section conflicts
  4. 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 with afterThrow: "halt" configuration. Triggers handleError in ProcessOptions.

  • ContinuousRejection: Created when an error should be collected but not halt execution. Used by advice types with afterThrow: "continue" configuration. Collected and passed to handleContinuedErrors.

Error Origin Tracking:

  • occurredFrom: "target": Error thrown by the target function itself
  • occurredFrom: "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:

  1. 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
  2. Setup Phase (runProcess):

    • AsyncContext is initialized with context generator
    • Context sections are locked based on advice declarations
    • Error handlers are attached to execution chain
  3. Execution Phase:

    • before advice executes with configurable parallelism
    • around advice builds wrapper chains (target then result)
    • Target executes within composed wrappers
    • afterReturning or afterThrowing based on outcome
    • after always executes (finally semantics)
  4. 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) * 2

Composition 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

  1. 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 });
  2. 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 };
    };
  3. AsyncContext Reuse: Use runProcessWith for high-frequency executions

    class 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

  1. No Dynamic Aspect Composition: Aspects must be known at process compilation time for performance and type safety

  2. Section-Based Locking: Prevents data races but requires upfront section declaration

  3. AsyncLocalStorage Dependency: Requires Node.js 16+, not suitable for browsers

  4. Synchronous Advice Not Supported: All advice must return Promise<void> for consistent async handling

  5. Process 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 lint

License

MIT © h1ylabs