UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

256 lines (223 loc) 7.63 kB
/* eslint-disable id-length */ /* eslint-disable symbol-description */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ /* eslint-disable @typescript-eslint/naming-convention */ import type { CapabilityMapping, Entity } from '../util/Types'; import type { SklHookContext } from './types'; // Use symbols for hook types to avoid string collisions export const HookTypes = { CREATE: Symbol('create'), READ: Symbol('read'), UPDATE: Symbol('update'), DELETE: Symbol('delete'), EXECUTE_CAPABILITY_MAPPING: Symbol('executeCapabilityMapping') }; export const HookStages = { BEFORE: Symbol('before'), AFTER: Symbol('after'), ERROR: Symbol('error') }; // Hook function types export type GlobalBeforeHook = (context: SklHookContext) => Promise<void> | void; export type GlobalAfterHook = (context: SklHookContext, result: any) => Promise<any> | any; export type GlobalErrorHook = (context: SklHookContext, error: Error) => Promise<void> | void; export type GlobalExecuteCapabilityHook = ( context: SklHookContext, capabilityMapping: CapabilityMapping, ) => Promise<void> | void; // Hook registry entry type with metadata interface HookEntry { id: symbol; fn: GlobalBeforeHook | GlobalAfterHook | GlobalErrorHook; priority: number; } // Registry to store global hooks class GlobalHooksRegistry { private readonly hooks: Map<symbol, Map<symbol, HookEntry[]>> = new Map(); constructor() { // Initialize hook maps for all CRUD operations and stages Object.values(HookTypes).forEach(type => { this.hooks.set(type, new Map()); Object.values(HookStages).forEach(stage => { this.hooks.get(type)!.set(stage, []); }); }); } /** * Register a hook with optional priority (higher runs first) */ register( type: symbol, stage: symbol, hook: GlobalBeforeHook | GlobalAfterHook | GlobalErrorHook, priority = 0 ): symbol { const hookId = Symbol(); const hooksList = this.hooks.get(type)?.get(stage); if (!hooksList) { throw new Error(`Invalid hook type or stage`); } hooksList.push({ id: hookId, fn: hook, priority }); // Sort by priority (descending) hooksList.sort((a, b) => b.priority - a.priority); return hookId; } /** * Unregister a hook by its ID */ unregister(hookId: symbol): boolean { let removed = false; this.hooks.forEach(stageMap => { stageMap.forEach(hooksList => { const index = hooksList.findIndex(entry => entry.id === hookId); if (index !== -1) { hooksList.splice(index, 1); removed = true; } }); }); return removed; } /** * Execute hooks for a specific operation and stage */ async execute(type: symbol, stage: symbol, context: SklHookContext, resultOrError?: any): Promise<any> { // Allow bypassing hooks to prevent re-entrancy from within hooks if (context?.bypassHooks) { return resultOrError; } const hooksList = this.hooks.get(type)?.get(stage); if (!hooksList || hooksList.length === 0) { return resultOrError; } if (stage === HookStages.BEFORE) { for (const entry of hooksList) { await (entry.fn as GlobalBeforeHook)(context); } return resultOrError; } if (stage === HookStages.AFTER) { let result = resultOrError; for (const entry of hooksList) { const newResult = await (entry.fn as GlobalAfterHook)(context, result); if (newResult !== undefined) { result = newResult; } } return result; } if (stage === HookStages.ERROR) { for (const entry of hooksList) { await (entry.fn as GlobalErrorHook)(context, resultOrError); } return resultOrError; } return resultOrError; } // Convenience methods for common operations registerBeforeCreate(hook: GlobalBeforeHook, priority?: number): symbol { return this.register(HookTypes.CREATE, HookStages.BEFORE, hook, priority); } registerAfterCreate(hook: GlobalAfterHook, priority?: number): symbol { return this.register(HookTypes.CREATE, HookStages.AFTER, hook, priority); } registerErrorCreate(hook: GlobalErrorHook, priority?: number): symbol { return this.register(HookTypes.CREATE, HookStages.ERROR, hook, priority); } // Additional convenience methods for other CRUD operations registerBeforeRead(hook: GlobalBeforeHook, priority?: number): symbol { return this.register(HookTypes.READ, HookStages.BEFORE, hook, priority); } // ... other convenience methods for read, update, delete operations // Convenience methods for execute capability operations registerBeforeExecuteCapabilityMapping(hook: GlobalExecuteCapabilityHook, priority?: number): symbol { return this.register(HookTypes.EXECUTE_CAPABILITY_MAPPING, HookStages.BEFORE, hook, priority); } registerAfterExecuteCapabilityMapping(hook: GlobalAfterHook, priority?: number): symbol { return this.register(HookTypes.EXECUTE_CAPABILITY_MAPPING, HookStages.AFTER, hook, priority); } registerErrorExecuteCapabilityMapping(hook: GlobalErrorHook, priority?: number): symbol { return this.register(HookTypes.EXECUTE_CAPABILITY_MAPPING, HookStages.ERROR, hook, priority); } // Execution convenience methods async executeBeforeCreate(entities: Entity[], extras?: Partial<SklHookContext>): Promise<void> { await this.execute( HookTypes.CREATE, HookStages.BEFORE, { entities, operation: 'save', operationParameters: {}, ...extras} ); } async executeAfterCreate(entities: Entity | Entity[], extras?: Partial<SklHookContext>): Promise<any> { if (!Array.isArray(entities)) { entities = [ entities ]; } return this.execute( HookTypes.CREATE, HookStages.AFTER, { entities, operation: 'save', operationParameters: {}, ...extras}, entities ); } async executeErrorCreate(entities: Entity[], error: Error, extras?: Partial<SklHookContext>): Promise<void> { await this.execute( HookTypes.CREATE, HookStages.ERROR, { entities, operation: 'save', operationParameters: {}, ...extras}, error ); } async executeBeforeExecuteCapabilityMapping( entities: Entity[], capabilityMapping: CapabilityMapping, extras?: Partial<SklHookContext> ): Promise<void> { await this.execute(HookTypes.EXECUTE_CAPABILITY_MAPPING, HookStages.BEFORE, { entities, operation: 'executeCapabilityMapping', operationParameters: { capabilityMapping }, ...extras }); } async executeAfterExecuteCapabilityMapping( entities: Entity[], capabilityMapping: CapabilityMapping, result: any, extras?: Partial<SklHookContext> ): Promise<any> { return this.execute( HookTypes.EXECUTE_CAPABILITY_MAPPING, HookStages.AFTER, { entities, operation: 'executeCapabilityMapping', operationParameters: { capabilityMapping }, ...extras }, result ); } async executeErrorExecuteCapabilityMapping( entities: Entity[], capabilityMapping: CapabilityMapping, error: Error, extras?: Partial<SklHookContext> ): Promise<void> { await this.execute( HookTypes.EXECUTE_CAPABILITY_MAPPING, HookStages.ERROR, { entities, operation: 'executeCapabilityMapping', operationParameters: { capabilityMapping }, ...extras }, error ); } } // Export a singleton instance export const globalHooks = new GlobalHooksRegistry();