npm.io
1.1.72 • Published 5d ago

@jucie.io/engine

Licence
SEE LICENSE IN ../LICENSE
Version
1.1.72
Deps
1
Size
175 kB
Vulns
0
Weekly
52

@jucie.io/engine

A powerful, extensible service engine for JavaScript applications with middleware support, async operations, and comprehensive lifecycle management.

Important Notice

This package is made publicly available for use but comes with no support, maintenance commitments, or warranty of any kind. It is provided strictly "as-is" under the MIT license.

  • Issues and feature requests may not be addressed
  • No timeline or commitment for updates
  • Not actively seeking contributions
  • Use at your own risk

If you choose to use this package, please ensure it meets your needs through your own testing and evaluation.

Features

  • Service Architecture: Clean, modular service system with lifecycle management
  • Middleware Support: Composable middleware with sync/async support
  • Async Operations: Full async/await support for actions and middleware
  • Security: Built-in protection against prototype pollution and namespace conflicts
  • Dependency Management: Automatic service dependency resolution
  • Configuration: Flexible service configuration with defaults merging
  • Well Tested: Comprehensive test suite with 33+ test cases
  • TypeScript Ready: Full TypeScript definitions included

Installation

npm install @jucie.io/engine

Quick Start

Method Pattern
import { Engine, ServiceProvider } from '@jucie.io/engine';

// Create an engine instance
const engine = Engine.create();

// Define a service using method pattern
class MyService extends ServiceProvider {
  static manifest = {
    name: 'My Service',
    namespace: 'myService',
    version: '1.0.0'
  };

  actions(inject, config) {
    return {
      greet: (name) => `Hello, ${name}!`,
      fetchData: async () => {
        // Async operations supported
        return await fetch('/api/data').then(r => r.json());
      }
    };
  }

  middleware(inject, config) {
    return (action, ctx, next) => {
      console.log(`Executing action: ${action}`);
      return next();
    };
  }
}

// Install the service
await engine.install(MyService);

// Use the service
const greeting = engine.myService.greet('World');
console.log(greeting); // "Hello, World!"
Setup Pattern
import { Engine, ServiceProvider } from '@jucie.io/engine';

// Create an engine instance
const engine = Engine.create();

// Define a service using setup pattern
class MyService extends ServiceProvider {
  static manifest = {
    name: 'My Service',
    namespace: 'myService',
    version: '1.0.0'
  };

  setup({ defineActions, defineMiddleware }) {
    defineActions((inject, config) => {
      return {
        greet: (name) => `Hello, ${name}!`,
        fetchData: async () => {
          return await fetch('/api/data').then(r => r.json());
        }
      };
    });

    defineMiddleware((inject, config) => {
      return (action, ctx, next) => {
        console.log(`Executing action: ${action}`);
        return next();
      };
    });
  }
}

// Install the service
await engine.install(MyService);

// Use the service
const greeting = engine.myService.greet('World');
console.log(greeting); // "Hello, World!"

Service Architecture

ServiceProvider Base Class

All services extend the ServiceProvider base class and implement the following lifecycle methods:

Required Properties
static manifest = {
  name: 'Service Name',
  namespace: 'serviceName',  // Must be valid JS identifier
  version: '1.0.0'
};
Service Definition Patterns

The ServiceProvider supports two patterns for defining service functionality:

Method Pattern (Traditional approach):

  • actions(inject, config) - Define service actions (required)
  • middleware(inject, config) - Define middleware functions (optional)
  • getters(inject, config) - Define read-only properties (optional)
  • initialize(inject, config) - Setup logic called directly (optional)
  • uninstall(inject, config) - Cleanup logic called directly (optional)

Setup Pattern (Declarative approach):

  • setup({ defineActions, defineMiddleware, defineGetters, defineInitialize, defineUninstall }) - Declarative service definition
The inject Function

Every lifecycle method (actions, middleware, getters, initialize, uninstall) receives inject as its first argument. It provides access to other installed services and built-in context (such as log, relay, cache, and config).

Return behaviour depends on how many keys you pass:

// No arguments → returns the full context object
const ctx = inject();

// Single key → returns that value directly (not wrapped)
const state = inject('state');

// Multiple keys → returns an array, suitable for destructuring
const [state, log] = inject('state', 'log');
const [auth, db, relay] = inject('auth', 'db', 'relay');

The multi-key form is the cleanest way to pull several dependencies at the top of a method:

actions(inject, config) {
  const [auth, db] = inject('auth', 'db');
  return {
    getUser: (id) => auth.verify(id) && db.query('SELECT * FROM users WHERE id = ?', [id])
  };
}
Method Pattern Example
class DatabaseService extends ServiceProvider {
  static manifest = {
    name: 'Database Service',
    namespace: 'db',
    version: '1.0.0',
    defaults: { connectionString: 'sqlite:memory:' }
  };

  #connection = null;

  initialize(inject, config) {
    this.#connection = createConnection(this.config.connectionString);
  }

  getters(inject, config) {
    return {
      isConnected: () => this.#connection !== null
    };
  }

  middleware(inject, config) {
    return (action, ctx, next) => {
      console.log(`Database action: ${action}`);
      return next();
    };
  }

  actions(inject, config) {
    return {
      async query: (sql, params = []) => {
        return await this.#connection.query(sql, params);
      },

      async transaction: async (callback) => {
        const tx = await this.#connection.beginTransaction();
        try {
          const result = await callback(tx);
          await tx.commit();
          return result;
        } catch (error) {
          await tx.rollback();
          throw error;
        }
      }
    };
  }

  uninstall(inject, config) {
    if (this.#connection) {
      this.#connection.close();
      this.#connection = null;
    }
  }
}
Setup Pattern Example
class CacheService extends ServiceProvider {
  static manifest = {
    name: 'Cache Service',
    namespace: 'cache',
    version: '1.0.0',
    defaults: { ttl: 300000, maxSize: 1000 }
  };

  #cache = new Map();

  setup({ defineMiddleware, defineGetters, defineActions, defineInitialize, defineUninstall }) {
    // Define middleware
    defineMiddleware((inject, config) => {
      return (action, ctx, next) => {
        console.log(`Cache action: ${action}`);
        return next();
      };
    });

    // Define getters
    defineGetters((inject, config) => {
      return {
        size: () => this.#cache.size,
        isEnabled: () => this.config.maxSize > 0
      };
    });

    // Define actions
    defineActions((inject, config) => {
      return {
        get: (key) => {
          const item = this.#cache.get(key);
          if (!item) return null;
          
          if (Date.now() > item.expires) {
            this.#cache.delete(key);
            return null;
          }
          
          return item.value;
        },

        set: (key, value, ttl = this.config.ttl) => {
          if (this.#cache.size >= this.config.maxSize) {
            // Remove oldest entry
            const firstKey = this.#cache.keys().next().value;
            this.#cache.delete(firstKey);
          }
          
          this.#cache.set(key, {
            value,
            expires: Date.now() + ttl
          });
        },

        clear: () => {
          this.#cache.clear();
        }
      };
    });

    // Define initialize
    defineInitialize((inject, config) => {
      console.log('Cache service initialized');
    });

    // Define uninstall
    defineUninstall((inject, config) => {
      this.#cache.clear();
      console.log('Cache service cleaned up');
    });
  }
}
Pattern Comparison
Feature Method Pattern Setup Pattern
Syntax Class methods Single setup function
Organization Separate methods Centralized definition
Flexibility High - conditional methods Medium - declarative
Readability Good for simple services Excellent for complex services
Type Safety Standard class methods Requires proper typing
Use Case Simple to medium services Complex services with many definitions

Middleware System

The engine supports powerful middleware composition for intercepting and processing action calls:

class LoggingService extends ServiceProvider {
  static manifest = {
    name: 'Logging Service',
    namespace: 'logging',
    version: '1.0.0'
  };

  middleware(inject, config) {
    return [
      // Sync middleware
      (action, ctx, next) => {
        console.log(`[${new Date().toISOString()}] Starting ${action}`);
        const start = Date.now();
        
        const result = next();
        
        console.log(`[${new Date().toISOString()}] Completed ${action} in ${Date.now() - start}ms`);
        return result;
      },

      // Async middleware
      async (action, ctx, next) => {
        // Authentication check
        const user = await authenticateUser();
        if (!user) {
          throw new Error('Unauthorized');
        }
        
        ctx.user = user;
        return next();
      }
    ];
  }

  actions(inject, config) {
    return {
      log: (message) => console.log(message)
    };
  }
}

Service Dependencies

Services can declare dependencies that are automatically installed:

class AuthService extends ServiceProvider {
  static manifest = {
    name: 'Auth Service',
    namespace: 'auth',
    version: '1.0.0'
  };

  actions(inject, config) {
    const db = inject('db');
    return {
      login: (credentials) => {
        return db.query('SELECT * FROM users WHERE email = ?', [credentials.email]);
      }
    };
  }
}

class ApiService extends ServiceProvider {
  static manifest = {
    name: 'API Service',
    namespace: 'api',
    version: '1.0.0',
    dependencies: [AuthService, DatabaseService] // Auto-install dependencies
  };

  actions(inject, config) {
    const [auth, db] = inject('auth', 'db');
    return {
      handleRequest: (req, res) => {
        const user = auth.getCurrentUser();
        return db.query('SELECT * FROM posts WHERE userId = ?', [user.id]);
      }
    };
  }
}

Configuration

Services support configuration with defaults:

class ConfigurableService extends ServiceProvider {
  static manifest = {
    name: 'Configurable Service',
    namespace: 'configurable',
    version: '1.0.0',
    defaults: {
      timeout: 5000,
      retries: 3,
      debug: false
    }
  };

  actions(inject, config) {
    return {
      getConfig: () => this.config,
      getTimeout: () => this.config.timeout
    };
  }
}

// Install with custom configuration
const configuredService = ConfigurableService.configure({
  timeout: 10000,
  debug: true
});

await engine.install(configuredService);
console.log(engine.configurable.getTimeout()); // 10000

Global Engine Context

The engine provides global context management:

import { provideEngine, useEngine, hasEngine } from '@jucie.io/engine';

// Provide engine globally
provideEngine(engine);

// Use engine from anywhere
const globalEngine = useEngine();
const hasGlobalEngine = hasEngine();

Error Handling

The engine provides comprehensive error handling with context:

class ErrorProneService extends ServiceProvider {
  static manifest = {
    name: 'Error Service',
    namespace: 'error',
    version: '1.0.0'
  };

  actions(inject, config) {
    return {
      riskyOperation: () => {
        throw new Error('Something went wrong');
      }
    };
  }
}

try {
  await engine.install(ErrorProneService);
  engine.error.riskyOperation();
} catch (error) {
  console.log(error.action);     // 'riskyOperation'
  console.log(error.namespace);  // 'error'
  console.log(error.message);    // 'Something went wrong'
}

Security Features

The engine includes built-in security protections:

  • Prototype Pollution Prevention: Blocks __proto__ and constructor namespace usage
  • Namespace Validation: Ensures valid JavaScript identifiers
  • Reserved Namespace Protection: Prevents conflicts with engine methods
  • Input Sanitization: Validates service manifests and configurations

API Reference

Engine
Engine.create(config?)

Creates a new engine instance.

engine.install(...services)

Installs one or more services.

engine.uninstall(...namespaces)

Uninstalls services by namespace.

engine.use(...definitions)

Adds middleware or other definitions directly.

ServiceProvider
ServiceProvider.configure(options)

Creates a configured service instance.

Static Properties
  • manifest - Service metadata and configuration
Lifecycle Methods
  • actions(inject, config) - Define service actions
  • middleware(inject, config) - Define middleware functions
  • getters(inject, config) - Define read-only properties
  • initialize(inject, config) - Setup logic called directly
  • uninstall(inject, config) - Cleanup logic called directly

Testing

The engine includes a comprehensive test suite:

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

Development

# Install dependencies
npm install

# Build the project
npm run build

# Watch for changes
npm run watch

# Run benchmarks
npm run bench

License

MIT License with Commons Clause - See LICENSE file for details.

TL;DR: Free to use in your projects, but you cannot sell this software as a competing product or service.

Keywords