From 195b624e606e3de2dc0ad8fe36e863ce24a56487 Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Wed, 27 Mar 2024 10:26:33 +0100 Subject: [PATCH 1/3] feat(plugins): draft implementation of golem plugins --- src/plugins/examplePlugin.ts | 34 ++++++++++++++++++++++ src/plugins/globalPluginManager.ts | 46 ++++++++++++++++++++++++++++++ src/plugins/marketPluginContext.ts | 17 +++++++++++ src/plugins/pluginContext.ts | 22 ++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 src/plugins/examplePlugin.ts create mode 100644 src/plugins/globalPluginManager.ts create mode 100644 src/plugins/marketPluginContext.ts create mode 100644 src/plugins/pluginContext.ts diff --git a/src/plugins/examplePlugin.ts b/src/plugins/examplePlugin.ts new file mode 100644 index 000000000..682168af0 --- /dev/null +++ b/src/plugins/examplePlugin.ts @@ -0,0 +1,34 @@ +import { MarketApi } from "ya-ts-client"; +import { GlobalPluginManager, GolemPlugin, registerGlobalPlugin } from "./globalPluginManager"; + +const linearPricingOnlyPlugin: GolemPlugin = { + name: "linearPricingOnlyPlugin", + version: "1.0.0", + register(golem) { + golem.market.registerHook("beforeDemandPublished", (demand) => { + demand.properties["golem.com.pricing.model"] = "linear"; + return demand; + }); + + golem.market.registerHook("filterInitialProposal", (proposal) => { + if (proposal.properties["golem.com.pricing.model"] !== "linear") { + return { isAccepted: false, reason: "Invalid pricing model" }; + } + return { isAccepted: true }; + }); + + golem.market.on("demandPublished", (demand) => { + console.log("demand has been published", demand); + }); + }, +}; + +registerGlobalPlugin(linearPricingOnlyPlugin); + +// inside demand publishing logic +const createDemandDTO = () => ({}) as MarketApi.DemandDTO; +let demand = createDemandDTO(); +const hooks = GlobalPluginManager.getHooks("beforeDemandPublished"); +for (const hook of hooks) { + demand = await hook(demand); +} diff --git a/src/plugins/globalPluginManager.ts b/src/plugins/globalPluginManager.ts new file mode 100644 index 000000000..9df604334 --- /dev/null +++ b/src/plugins/globalPluginManager.ts @@ -0,0 +1,46 @@ +import EventEmitter from "eventemitter3"; +import { MarketEvents, MarketHooks, MarketPluginContext } from "./marketPluginContext"; + +type GolemPluginContext = { + market: MarketPluginContext; +}; +export type GolemPlugin = { + name: string; + version: string; + register(context: GolemPluginContext): void; +}; + +type AllHooks = MarketHooks; // | DeploymentHooks | PaymentHooks | ... +type AllEvents = MarketEvents; // | DeploymentEvents | PaymentEvents | ... + +export class GlobalPluginManager { + static eventEmitter = new EventEmitter(); + static hooks = new Map(); + + static registerHook(hookName: T, hook: AllHooks[T]) { + if (!this.hooks.has(hookName)) { + this.hooks.set(hookName, []); + } + this.hooks.get(hookName)!.push(hook as NonNullable); + } + + static registerPlugin(plugin: GolemPlugin) { + const ctx = { + market: new MarketPluginContext( + (eventName, callback) => GlobalPluginManager.eventEmitter.on(eventName, callback), + (hookName, hook) => GlobalPluginManager.registerHook(hookName, hook), + ), + // deployment: ... + // payment: ... + // ... + }; + plugin.register(ctx); + } + static getHooks(hookName: T): AllHooks[T][] { + return (this.hooks.get(hookName) || []) as AllHooks[T][]; + } +} + +export function registerGlobalPlugin(...plugins: GolemPlugin[]) { + plugins.forEach((plugin) => GlobalPluginManager.registerPlugin(plugin)); +} diff --git a/src/plugins/marketPluginContext.ts b/src/plugins/marketPluginContext.ts new file mode 100644 index 000000000..2e8c478a3 --- /dev/null +++ b/src/plugins/marketPluginContext.ts @@ -0,0 +1,17 @@ +import { PluginContext } from "./pluginContext"; +import { MarketApi } from "ya-ts-client"; +type DemandDTO = MarketApi.DemandDTO; +type ProposalDTO = MarketApi.ProposalDTO; + +type OkOrNot = { isAccepted: true } | { isAccepted: false; reason: string }; +export type MarketHooks = { + beforeDemandPublished: (demand: DemandDTO) => DemandDTO | Promise; + filterInitialProposal: (proposal: ProposalDTO) => OkOrNot | Promise; +}; + +export type MarketEvents = { + demandPublished: (demand: DemandDTO) => void; + initialProposalReceived: (proposal: ProposalDTO) => void; +}; + +export class MarketPluginContext extends PluginContext {} diff --git a/src/plugins/pluginContext.ts b/src/plugins/pluginContext.ts new file mode 100644 index 000000000..3e6cc3bd5 --- /dev/null +++ b/src/plugins/pluginContext.ts @@ -0,0 +1,22 @@ +import EventEmitter from "eventemitter3"; + +export abstract class PluginContext< + Hooks extends { [key: string]: (...args: never[]) => unknown }, + Events extends { [key: string]: (...args: never[]) => unknown }, +> { + constructor( + private registerEventFn: >( + eventName: T, + callback: EventEmitter.EventListener, + ) => void, + private registerHookFn: (hookName: keyof Hooks, hook: Hooks[keyof Hooks]) => void, + ) {} + + registerHook(hookName: T, hook: Hooks[T]) { + this.registerHookFn(hookName, hook); + } + + on>(eventName: T, callback: EventEmitter.EventListener) { + this.registerEventFn(eventName, callback); + } +} From e0bd7c8e9b8b0214e95a94729c27f971bdcbac20 Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Wed, 27 Mar 2024 12:32:26 +0100 Subject: [PATCH 2/3] feat(plugins): differentiate local and global plugins --- src/plugins/examplePlugin.ts | 51 ++++++++++++++++--- src/plugins/localPluginManager.ts | 23 +++++++++ ...lobalPluginManager.ts => pluginManager.ts} | 25 ++++++--- 3 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 src/plugins/localPluginManager.ts rename src/plugins/{globalPluginManager.ts => pluginManager.ts} (54%) diff --git a/src/plugins/examplePlugin.ts b/src/plugins/examplePlugin.ts index 682168af0..fda573b4c 100644 --- a/src/plugins/examplePlugin.ts +++ b/src/plugins/examplePlugin.ts @@ -1,6 +1,8 @@ import { MarketApi } from "ya-ts-client"; -import { GlobalPluginManager, GolemPlugin, registerGlobalPlugin } from "./globalPluginManager"; +import { GolemPlugin, registerGlobalPlugin } from "./pluginManager"; +import { LocalPluginManager } from "./localPluginManager"; +// plugin that will be registered globally const linearPricingOnlyPlugin: GolemPlugin = { name: "linearPricingOnlyPlugin", version: "1.0.0", @@ -23,12 +25,47 @@ const linearPricingOnlyPlugin: GolemPlugin = { }, }; +// plugin that will be registered on a particular demand instead of globally +const cpuArchitecturePlugin: GolemPlugin = { + name: "cpuArchitecturePlugin", + version: "1.0.0", + register(golem) { + golem.market.registerHook("beforeDemandPublished", (demand) => { + demand.properties["golem.com.cpu.architecture"] = "x86_64"; + return demand; + }); + }, +}; + registerGlobalPlugin(linearPricingOnlyPlugin); -// inside demand publishing logic -const createDemandDTO = () => ({}) as MarketApi.DemandDTO; -let demand = createDemandDTO(); -const hooks = GlobalPluginManager.getHooks("beforeDemandPublished"); -for (const hook of hooks) { - demand = await hook(demand); +class ExampleDemandManager { + private pluginManager = new LocalPluginManager(); + constructor(plugins?: GolemPlugin[]) { + if (plugins) { + plugins.forEach((plugin) => this.pluginManager.registerPlugin(plugin)); + } + } + + async publish() { + let demand: MarketApi.DemandDTO = { + properties: {}, + } as MarketApi.DemandDTO; + + const hooks = this.pluginManager.getHooks("beforeDemandPublished"); + for (const hook of hooks) { + demand = await hook(demand); + } + this.pluginManager.emitEvent("demandPublished", demand); + return demand; + } } + +const demandManager0 = new ExampleDemandManager([cpuArchitecturePlugin]); +const demandManager1 = new ExampleDemandManager(); + +const demandWithCpuArchitecture = await demandManager0.publish(); +const demandWithOnlyLinearPricing = await demandManager1.publish(); + +console.log(demandWithCpuArchitecture); +console.log(demandWithOnlyLinearPricing); diff --git a/src/plugins/localPluginManager.ts b/src/plugins/localPluginManager.ts new file mode 100644 index 000000000..366889fb4 --- /dev/null +++ b/src/plugins/localPluginManager.ts @@ -0,0 +1,23 @@ +import EventEmitter from "eventemitter3"; +import { MarketEvents, MarketHooks } from "./marketPluginContext"; +import { GlobalPluginManager, PluginManager } from "./pluginManager"; + +/** + * Plugin manager that will combine local and global plugins. + * Global plugins should be registered using `GlobalPluginManager`. + * Global hooks will be executed before local hooks, in order of registration. + */ +export class LocalPluginManager extends PluginManager { + getHooks(hookName: T): MarketHooks[T][] { + const localHooks = super.getHooks(hookName); + const globalHooks = GlobalPluginManager.getHooks(hookName); + return [...globalHooks, ...localHooks]; + } + public emitEvent( + eventName: T, + ...args: EventEmitter.ArgumentMap[Extract] + ): void { + GlobalPluginManager.emitEvent(eventName, ...args); + super.emitEvent(eventName, ...args); + } +} diff --git a/src/plugins/globalPluginManager.ts b/src/plugins/pluginManager.ts similarity index 54% rename from src/plugins/globalPluginManager.ts rename to src/plugins/pluginManager.ts index 9df604334..731c2548a 100644 --- a/src/plugins/globalPluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -13,22 +13,22 @@ export type GolemPlugin = { type AllHooks = MarketHooks; // | DeploymentHooks | PaymentHooks | ... type AllEvents = MarketEvents; // | DeploymentEvents | PaymentEvents | ... -export class GlobalPluginManager { - static eventEmitter = new EventEmitter(); - static hooks = new Map(); +export class PluginManager { + private eventEmitter = new EventEmitter(); + private hooks = new Map(); - static registerHook(hookName: T, hook: AllHooks[T]) { + private registerHook(hookName: T, hook: AllHooks[T]) { if (!this.hooks.has(hookName)) { this.hooks.set(hookName, []); } this.hooks.get(hookName)!.push(hook as NonNullable); } - static registerPlugin(plugin: GolemPlugin) { + public registerPlugin(plugin: GolemPlugin) { const ctx = { market: new MarketPluginContext( - (eventName, callback) => GlobalPluginManager.eventEmitter.on(eventName, callback), - (hookName, hook) => GlobalPluginManager.registerHook(hookName, hook), + (eventName, callback) => this.eventEmitter.on(eventName, callback), + (hookName, hook) => this.registerHook(hookName, hook), ), // deployment: ... // payment: ... @@ -36,11 +36,20 @@ export class GlobalPluginManager { }; plugin.register(ctx); } - static getHooks(hookName: T): AllHooks[T][] { + public getHooks(hookName: T): AllHooks[T][] { return (this.hooks.get(hookName) || []) as AllHooks[T][]; } + public emitEvent( + eventName: T, + ...args: EventEmitter.ArgumentMap[Extract] + ) { + this.eventEmitter.emit(eventName, ...args); + } } +// eslint-disable-next-line @typescript-eslint/naming-convention +export const GlobalPluginManager = new PluginManager(); + export function registerGlobalPlugin(...plugins: GolemPlugin[]) { plugins.forEach((plugin) => GlobalPluginManager.registerPlugin(plugin)); } From 7dd8bfee9b0598dc4df5aae12c529d36a89b377e Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Thu, 28 Mar 2024 11:05:31 +0100 Subject: [PATCH 3/3] chore: update plugins example and incorrect types in local manager --- src/plugins/examplePlugin.ts | 19 +++++++++---------- src/plugins/localPluginManager.ts | 9 ++++----- src/plugins/pluginManager.ts | 4 ++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/plugins/examplePlugin.ts b/src/plugins/examplePlugin.ts index fda573b4c..18b27351a 100644 --- a/src/plugins/examplePlugin.ts +++ b/src/plugins/examplePlugin.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { MarketApi } from "ya-ts-client"; import { GolemPlugin, registerGlobalPlugin } from "./pluginManager"; import { LocalPluginManager } from "./localPluginManager"; @@ -37,9 +38,7 @@ const cpuArchitecturePlugin: GolemPlugin = { }, }; -registerGlobalPlugin(linearPricingOnlyPlugin); - -class ExampleDemandManager { +class Demand { private pluginManager = new LocalPluginManager(); constructor(plugins?: GolemPlugin[]) { if (plugins) { @@ -61,11 +60,11 @@ class ExampleDemandManager { } } -const demandManager0 = new ExampleDemandManager([cpuArchitecturePlugin]); -const demandManager1 = new ExampleDemandManager(); - -const demandWithCpuArchitecture = await demandManager0.publish(); -const demandWithOnlyLinearPricing = await demandManager1.publish(); +registerGlobalPlugin(linearPricingOnlyPlugin); +const demand0 = new Demand([cpuArchitecturePlugin]); +const demand1 = new Demand(); -console.log(demandWithCpuArchitecture); -console.log(demandWithOnlyLinearPricing); +// 👇 this demand will have pricing model and architecture set by the plugins +const demandWithCpuArchitecture = await demand0.publish(); +// 👇 this demand will have only pricing model set by the global plugin +const demandWithOnlyLinearPricing = await demand1.publish(); diff --git a/src/plugins/localPluginManager.ts b/src/plugins/localPluginManager.ts index 366889fb4..4dba5130d 100644 --- a/src/plugins/localPluginManager.ts +++ b/src/plugins/localPluginManager.ts @@ -1,6 +1,5 @@ import EventEmitter from "eventemitter3"; -import { MarketEvents, MarketHooks } from "./marketPluginContext"; -import { GlobalPluginManager, PluginManager } from "./pluginManager"; +import { AllEvents, AllHooks, GlobalPluginManager, PluginManager } from "./pluginManager"; /** * Plugin manager that will combine local and global plugins. @@ -8,14 +7,14 @@ import { GlobalPluginManager, PluginManager } from "./pluginManager"; * Global hooks will be executed before local hooks, in order of registration. */ export class LocalPluginManager extends PluginManager { - getHooks(hookName: T): MarketHooks[T][] { + getHooks(hookName: T): AllHooks[T][] { const localHooks = super.getHooks(hookName); const globalHooks = GlobalPluginManager.getHooks(hookName); return [...globalHooks, ...localHooks]; } - public emitEvent( + public emitEvent( eventName: T, - ...args: EventEmitter.ArgumentMap[Extract] + ...args: EventEmitter.ArgumentMap[Extract] ): void { GlobalPluginManager.emitEvent(eventName, ...args); super.emitEvent(eventName, ...args); diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 731c2548a..57ff417b3 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -10,8 +10,8 @@ export type GolemPlugin = { register(context: GolemPluginContext): void; }; -type AllHooks = MarketHooks; // | DeploymentHooks | PaymentHooks | ... -type AllEvents = MarketEvents; // | DeploymentEvents | PaymentEvents | ... +export type AllHooks = MarketHooks; // | DeploymentHooks | PaymentHooks | ... +export type AllEvents = MarketEvents; // | DeploymentEvents | PaymentEvents | ... export class PluginManager { private eventEmitter = new EventEmitter();