Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add custom hook support #635

Merged
merged 14 commits into from
Jan 15, 2025
Merged
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;
TungstnBallon marked this conversation as resolved.
Show resolved Hide resolved
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
Loading