Skip to content

Commit

Permalink
Merge pull request #635 from jvalue/hooks
Browse files Browse the repository at this point in the history
[FEATURE] Add custom hook support
  • Loading branch information
TungstnBallon authored Jan 15, 2025
2 parents 2029836 + 81b41be commit a5191a4
Show file tree
Hide file tree
Showing 10 changed files with 604 additions and 15 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ node_modules
*.launch
.settings/
*.sublime-workspace
.nvim.lua
.nvimrc
.exrc

# IDE - VSCode
.vscode/*
Expand Down Expand Up @@ -58,4 +61,4 @@ Thumbs.db
.cache-loader/

.nx/cache
.nx/workspace-data
.nx/workspace-data
4 changes: 4 additions & 0 deletions libs/execution/src/lib/blocks/block-execution-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,15 @@ export async function executeBlocks(

executionContext.enterNode(block);

await executionContext.executeHooks(inputValue);

const executionResult = await executeBlock(
inputValue,
block,
executionContext,
);
await executionContext.executeHooks(inputValue, executionResult);

if (R.isErr(executionResult)) {
return executionResult;
}
Expand Down
33 changes: 33 additions & 0 deletions libs/execution/src/lib/execution-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

// eslint-disable-next-line unicorn/prefer-node-protocol
import { strict as assert } from 'assert';
import { inspect } from 'node:util';

import {
type BlockDefinition,
Expand All @@ -26,13 +27,16 @@ import {
} from '@jvalue/jayvee-language-server';
import { assertUnreachable, isReference } from 'langium';

import { type Result } from './blocks';
import { type JayveeConstraintExtension } from './constraints';
import {
type DebugGranularity,
type DebugTargets,
} from './debugging/debug-configuration';
import { type JayveeExecExtension } from './extension';
import { type HookContext } from './hooks';
import { type Logger } from './logging/logger';
import { type IOTypeImplementation } from './types';

export type StackNode =
| BlockDefinition
Expand All @@ -55,6 +59,7 @@ export class ExecutionContext {
debugTargets: DebugTargets;
},
public readonly evaluationContext: EvaluationContext,
public readonly hookContext: HookContext,
) {
logger.setLoggingContext(pipeline.name);
}
Expand Down Expand Up @@ -133,6 +138,34 @@ export class ExecutionContext {
return property;
}

public executeHooks(
input: IOTypeImplementation | null,
output?: Result<IOTypeImplementation | null>,
) {
const node = this.getCurrentNode();
assert(
isBlockDefinition(node),
`Expected node to be \`BlockDefinition\`: ${inspect(node)}`,
);

const blocktype = node.type.ref?.name;
assert(
blocktype !== undefined,
`Expected block definition to have a blocktype: ${inspect(node)}`,
);

if (output === undefined) {
return this.hookContext.executePreBlockHooks(blocktype, input, this);
}

return this.hookContext.executePostBlockHooks(
blocktype,
input,
this,
output,
);
}

private getDefaultPropertyValue<I extends InternalValueRepresentation>(
propertyName: string,
valueType: ValueType<I>,
Expand Down
161 changes: 161 additions & 0 deletions libs/execution/src/lib/hooks/hook-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2025 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import { type Result } from '../blocks';
import { type ExecutionContext } from '../execution-context';
import { type IOTypeImplementation } from '../types';

import {
type HookOptions,
type HookPosition,
type PostBlockHook,
type PreBlockHook,
isPreBlockHook,
} from './hook';

const AllBlocks = '*';

interface HookSpec<H extends PreBlockHook | PostBlockHook> {
blocking: boolean;
hook: H;
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
function noop() {}

async function executePreBlockHooks(
hooks: HookSpec<PreBlockHook>[],
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
) {
await Promise.all(
hooks.map(async ({ blocking, hook }) => {
if (blocking) {
await hook(blocktype, input, context);
} else {
hook(blocktype, input, context).catch(noop);
}
}),
);
}

async function executePostBlockHooks(
hooks: HookSpec<PostBlockHook>[],
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output: Result<IOTypeImplementation | null>,
) {
await Promise.all(
hooks.map(async ({ blocking, hook }) => {
if (blocking) {
await hook(blocktype, input, output, context);
} else {
hook(blocktype, input, output, context).catch(noop);
}
}),
);
}

export class HookContext {
private hooks: {
pre: Record<string, HookSpec<PreBlockHook>[]>;
post: Record<string, HookSpec<PostBlockHook>[]>;
} = { pre: {}, post: {} };

public addHook(
position: 'preBlock',
hook: PreBlockHook,
opts: HookOptions,
): void;
public addHook(
position: 'postBlock',
hook: PostBlockHook,
opts: HookOptions,
): void;
public addHook(
position: HookPosition,
hook: PreBlockHook | PostBlockHook,
opts: HookOptions,
): void;
public addHook(
position: HookPosition,
hook: PreBlockHook | PostBlockHook,
opts: HookOptions,
) {
for (const blocktype of opts.blocktypes ?? [AllBlocks]) {
if (isPreBlockHook(hook, position)) {
if (this.hooks.pre[blocktype] === undefined) {
this.hooks.pre[blocktype] = [];
}
this.hooks.pre[blocktype].push({
blocking: opts.blocking ?? false,
hook,
});
} else {
if (this.hooks.post[blocktype] === undefined) {
this.hooks.post[blocktype] = [];
}
this.hooks.post[blocktype].push({
blocking: opts.blocking ?? false,
hook,
});
}
}
}

public async executePreBlockHooks(
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
) {
context.logger.logInfo(`Executing general pre-block-hooks`);
const general = executePreBlockHooks(
this.hooks.pre[AllBlocks] ?? [],
blocktype,
input,
context,
);
context.logger.logInfo(
`Executing pre-block-hooks for blocktype ${blocktype}`,
);
const blockSpecific = executePreBlockHooks(
this.hooks.pre[blocktype] ?? [],
blocktype,
input,
context,
);

await Promise.all([general, blockSpecific]);
}

public async executePostBlockHooks(
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output: Result<IOTypeImplementation | null>,
) {
context.logger.logInfo(`Executing general post-block-hooks`);
const general = executePostBlockHooks(
this.hooks.post[AllBlocks] ?? [],
blocktype,
input,
context,
output,
);
context.logger.logInfo(
`Executing post-block-hooks for blocktype ${blocktype}`,
);
const blockSpecific = executePostBlockHooks(
this.hooks.post[blocktype] ?? [],
blocktype,
input,
context,
output,
);

await Promise.all([general, blockSpecific]);
}
}
46 changes: 46 additions & 0 deletions libs/execution/src/lib/hooks/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import { type Result } from '../blocks';
import { type ExecutionContext } from '../execution-context';
import { type IOTypeImplementation } from '../types';

/** When to execute the hook.*/
export type HookPosition = 'preBlock' | 'postBlock';

export interface HookOptions {
/** Whether the pipeline should await the hooks completion. `false` if omitted.*/
blocking?: boolean;
/** Optionally specify one or more blocks to limit this hook to. If omitted, the hook will be executed on all blocks*/
blocktypes?: string[];
}

/** This function will be executed before a block.*/
export type PreBlockHook = (
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
) => Promise<void>;

export function isPreBlockHook(
hook: PreBlockHook | PostBlockHook,
position: HookPosition,
): hook is PreBlockHook {
return position === 'preBlock';
}

/** This function will be executed before a block.*/
export type PostBlockHook = (
blocktype: string,
input: IOTypeImplementation | null,
output: Result<IOTypeImplementation | null>,
context: ExecutionContext,
) => Promise<void>;

export function isPostBlockHook(
hook: PreBlockHook | PostBlockHook,
position: HookPosition,
): hook is PostBlockHook {
return position === 'postBlock';
}
6 changes: 6 additions & 0 deletions libs/execution/src/lib/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2025 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

export * from './hook';
export * from './hook-context';
1 change: 1 addition & 0 deletions libs/execution/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './types/value-types/visitors';
export * from './execution-context';
export * from './extension';
export * from './logging';
export * from './hooks';
Loading

0 comments on commit a5191a4

Please sign in to comment.