@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
256 lines (223 loc) • 7.63 kB
text/typescript
/* 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();