Skip to content

Commit

Permalink
fix: postBlock hooks recieve the block's output
Browse files Browse the repository at this point in the history
  • Loading branch information
TungstnBallon committed Jan 10, 2025
1 parent 0965249 commit e2acb84
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 95 deletions.
2 changes: 2 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,13 @@ export async function executeBlocks(

executionContext.enterNode(block);

await executionContext.executeHooks(inputValue); // FIXME #634: Pass the blocktype to also execute block specific hooks
const executionResult = await executeBlock(
inputValue,
block,
executionContext,
);
await executionContext.executeHooks(inputValue, executionResult); // FIXME #634: Pass the blocktype to also execute block specific hooks
if (R.isErr(executionResult)) {
return executionResult;
}
Expand Down
3 changes: 0 additions & 3 deletions libs/execution/src/lib/blocks/block-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,12 @@ export abstract class AbstractBlockExecutor<I extends IOType, O extends IOType>
input: IOTypeImplementation<I>,
context: ExecutionContext,
): Promise<R.Result<IOTypeImplementation<O> | null>> {
await context.executeHooks('before', input, context); // FIXME #634: Pass the blocktype to also execute block specific hooks

const executionResult = await this.doExecute(input, context);

if (R.isOk(executionResult)) {
this.logBlockResult(executionResult.right, context);
}

await context.executeHooks('after', input, context); // FIXME #634: Pass the blocktype to also execute block specific hooks
return executionResult;
}

Expand Down
13 changes: 6 additions & 7 deletions libs/execution/src/lib/execution-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import {
} from '@jvalue/jayvee-language-server';
import { assertUnreachable, isReference } from 'langium';

import { type BlockExecutor } from './blocks';
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 Hook, type HookContext } from './hooks';
import { type HookContext, type HookPosition } from './hooks';
import { type Logger } from './logging/logger';
import { type IOTypeImplementation } from './types';

Expand Down Expand Up @@ -138,10 +138,9 @@ export class ExecutionContext {
return property;
}

public async executeHooks(
position: 'before' | 'after',
input: IOTypeImplementation,
context: ExecutionContext,
public executeHooks(
input: IOTypeImplementation | null,
output?: Result<IOTypeImplementation | null>,
) {
const node = this.getCurrentNode();
assert(
Expand All @@ -155,7 +154,7 @@ export class ExecutionContext {
`Expected block definition to have a blocktype: ${inspect(node)}`,
);

return this.hookContext.executeHooks(position, blocktype, input, context);
return this.hookContext.executeHooks(blocktype, input, this, output);
}

private getDefaultPropertyValue<I extends InternalValueRepresentation>(
Expand Down
183 changes: 183 additions & 0 deletions libs/execution/src/lib/hooks/hook-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// eslint-disable-next-line unicorn/prefer-node-protocol
import assert from 'assert';

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;
}

async function executeTheseHooks(
hooks: HookSpec<PreBlockHook>[],
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
): Promise<void>;
async function executeTheseHooks(
hooks: HookSpec<PostBlockHook>[],
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output: Result<IOTypeImplementation | null>,
): Promise<void>;
async function executeTheseHooks(
hooks: HookSpec<PreBlockHook>[] | HookSpec<PostBlockHook>[],
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output?: Result<IOTypeImplementation | null>,
) {
const position = output === undefined ? 'preBlock' : 'postBlock';
return (
Promise.all(
hooks.map(async ({ blocking, hook }) => {
if (blocking) {
if (isPreBlockHook(hook, position)) {
await hook(blocktype, input, context);
} else {
assert(output !== undefined, 'Guarnteed to be a postBlock hook');
await hook(blocktype, input, output, context);
}
} else {
if (isPreBlockHook(hook, position)) {
hook(blocktype, input, context)
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
} else {
assert(output !== undefined, 'Guarnteed to be a postBlock hook');
hook(blocktype, input, output, context)
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
}
}
}),
)
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
);
}

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,
) {
const blocktypes: string[] =
typeof opts.blocktypes === 'string'
? [opts.blocktypes]
: opts.blocktypes ?? [AllBlocks];

blocktypes.forEach((blocktype) => {
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 executeHooks(
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
): Promise<void>;
public async executeHooks(
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output: Result<IOTypeImplementation | null>,
): Promise<void>;
public async executeHooks(
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output?: Result<IOTypeImplementation | null>,
): Promise<void>;
public async executeHooks(
blocktype: string,
input: IOTypeImplementation | null,
context: ExecutionContext,
output?: Result<IOTypeImplementation | null>,
) {
if (output === undefined) {
const general = executeTheseHooks(
this.hooks.pre[AllBlocks] ?? [],
blocktype,
input,
context,
);
const blockSpecific = executeTheseHooks(
this.hooks.pre[blocktype] ?? [],
blocktype,
input,
context,
);

// eslint-disable-next-line @typescript-eslint/no-empty-function
return Promise.all([general, blockSpecific]).then(() => {});
}
const general = executeTheseHooks(
this.hooks.post[AllBlocks] ?? [],
blocktype,
input,
context,
output,
);
const blockSpecific = executeTheseHooks(
this.hooks.post[blocktype] ?? [],
blocktype,
input,
context,
output,
);

// eslint-disable-next-line @typescript-eslint/no-empty-function
return Promise.all([general, blockSpecific]).then(() => {});
}
}
105 changes: 27 additions & 78 deletions libs/execution/src/lib/hooks/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,46 @@
//
// SPDX-License-Identifier: AGPL-3.0-only

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

/** A hook can be executed `before` or `after` a block*/
export type HookPosition = 'before' | 'after';

const AllBlocks = '*';

async function executeTheseHooks(
hooks: HookSpec[],
blocktype: string,
input: IOTypeImplementation,
context: ExecutionContext,
) {
await Promise.all(
hooks.map(async ({ blocking, hook }) => {
if (blocking) {
await hook(blocktype, input, context);
} else {
// eslint-disable-next-line @typescript-eslint/no-empty-function
hook(blocktype, input, context).catch(() => {});
}
}),
);
}

export class HookContext {
private hooks: {
before: Record<string, HookSpec[]>;
after: Record<string, HookSpec[]>;
} = { before: {}, after: {} };

public addHook(hook: Hook, opts: HookOptions) {
const blocktypes: string[] =
typeof opts.blocktypes === 'string'
? [opts.blocktypes]
: opts.blocktypes ?? [AllBlocks];

blocktypes.forEach((blocktype) => {
if (this.hooks[opts.position][blocktype] === undefined) {
this.hooks[opts.position][blocktype] = [];
}
this.hooks[opts.position][blocktype]?.push({
blocking: opts.blocking ?? false,
hook,
});
});
}

public async executeHooks(
position: HookPosition,
blocktype: string,
input: IOTypeImplementation,
context: ExecutionContext,
) {
const general = executeTheseHooks(
this.hooks[position][AllBlocks] ?? [],
blocktype,
input,
context,
);
const blockSpecific = executeTheseHooks(
this.hooks[position][blocktype] ?? [],
blocktype,
input,
context,
);

// eslint-disable-next-line @typescript-eslint/no-empty-function
return Promise.all([general, blockSpecific]).then(() => {});
}
}
/** When to execute the hook.*/
export type HookPosition = 'preBlock' | 'postBlock';

export interface HookOptions {
/** Whether the hook is executed `before` or `after` a block.*/
// FIXME: find a better name than `position`
position: HookPosition;
/** 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*/
// FIXME #634: Add `BlockExecutor` type
// FIXME #634: Add `BlockExecutor[]` variant
blocktypes?: string | string[];
}

export type Hook = (
/** 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,
input: IOTypeImplementation | null,
output: Result<IOTypeImplementation | null>,
context: ExecutionContext,
) => Promise<void>;

interface HookSpec {
blocking: boolean;
hook: Hook;
export function isPostBlockHook(
hook: PreBlockHook | PostBlockHook,
position: HookPosition,
): hook is PreBlockHook {
return position === 'postBlock';
}
1 change: 1 addition & 0 deletions libs/execution/src/lib/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
// SPDX-License-Identifier: AGPL-3.0-only

export * from './hook';
export * from './hook-context';
Loading

0 comments on commit e2acb84

Please sign in to comment.