From 76e8f710eb88e047f9afded73518d443bd64645c Mon Sep 17 00:00:00 2001 From: Grzegorz Godlewski Date: Tue, 19 Nov 2024 14:50:18 +0100 Subject: [PATCH] feat: introduced golem-network plugin api --- README.md | 1 + docs/PLUGINS.md | 135 +++++++++++++++++++++++++++++ src/golem-network/golem-network.ts | 30 +++++++ src/golem-network/index.ts | 1 + src/golem-network/plugin.ts | 38 ++++++++ 5 files changed, 205 insertions(+) create mode 100644 docs/PLUGINS.md create mode 100644 src/golem-network/plugin.ts diff --git a/README.md b/README.md index ac0c1cd58..49d4fc865 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ distributed, computational loads through Golem Network. disposal. - [Usage documentation](./docs/USAGE.md) - Explore supported usage and implementation patterns. - [Feature documentation](./docs/FEATURES.md) - Description of interesting features that we've prepared. +- [Plugin documentation](./docs/PLUGINS.md) - Learn how to write plugins that can enhance `GolemNetwork` user's experience by introducing code reusability and modularity. ## Installation diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md new file mode 100644 index 000000000..405e858af --- /dev/null +++ b/docs/PLUGINS.md @@ -0,0 +1,135 @@ +# Golem Network Plugins + + + +- [Golem Network Plugins](#golem-network-plugins) + - [Why do we need plugins?](#why-do-we-need-plugins) + - [Requestor script maintainability](#requestor-script-maintainability) + - [Opening the ecosystem for extensions](#opening-the-ecosystem-for-extensions) + - [Example plugins](#example-plugins) + - [Provider tracker](#provider-tracker) + - [Check GLM price before starting (asynchronous plugin)](#check-glm-price-before-starting-asynchronous-plugin) + - [Plugin lifecycle and cleanup](#plugin-lifecycle-and-cleanup) + - [Synchronous and asynchronous plugins](#synchronous-and-asynchronous-plugins) + + +Welcome to the Golem Network Plugins documentation! Here we aim to provide friendly and comprehensive guidance for +developers interested in creating reusable and generic modules for the Golem Network. + +## Why do we need plugins? + +### Requestor script maintainability + +As detailed in our [concepts](./CONCEPTS.md) guide, the SDK models the intricate domain of the Golem Network by defining +the `GolemNetwork` class, which is built from various subdomains of the project. + +If you've written a few `golem-js` scripts, you might have noticed your scripts growing quickly as you add logic for +market, activity, or payment events. This can make your main requestor script bulky and difficult to maintain. + +Golem Plugins offer a solution to this problem by providing a simple interface for attaching different types of logic to +your `GolemNetwork` instance. This allows you to break down your large scripts into smaller, more manageable modules and +use `GolemNetwork.use` to integrate them into your main script seamlessly. + +### Opening the ecosystem for extensions + +We warmly invite other developers to [contribute](./CONTRIBUTING.md) to our ecosystem. We recognize that creating core +SDK components requires considerable effort and dedication. + +To make contributions more accessible, we're introducing a new pathway for developers familiar with `golem-js` to create +their `GolemNetwork` plugins and share them via NPM. Let us know about your plugins so we can feature them in our +documentation. + +## Example plugins + +### Provider tracker + +Let's say you want to track unique providers on the network for statistical purposes. Here's an example plugin: + +```ts +import { GolemNetwork, GolemPluginInitializer } from "@golem-sdk/golem-js"; + +/** + * Example plugin that tracks unique provider ID/name pairs on the market + */ +const providerTracker: GolemPluginInitializer = (glm) => { + const seenProviders: { id: string; name: string }[] = []; + glm.market.events.on("offerProposalReceived", (event) => { + const { id, name } = event.offerProposal.provider; + const providerInfo = { id, name }; + if (!seenProviders.includes(providerInfo)) { + seenProviders.push(providerInfo); + console.log("Saw new provider %s named %s", id, name); + } + }); + // Return a cleanup function that will be executed during the `disconnect` + return () => { + console.log("Provider tracker found a total of %d providers", seenProviders.length); + }; +}; +``` + +You can connect this plugin to your main script as follows: + +```ts +const glm = new GolemNetwork(); +// Register the plugin that will be initialized during `connect` call +glm.use(providerTracker); +``` + +#### Check GLM price before starting (asynchronous plugin) + +If you want to avoid running your requestor script when the GLM price exceeds a certain USD value, you can capture this +policy with a reusable plugin: + +```ts +import { GolemNetwork, GolemPluginInitializer } from "@golem-sdk/golem-js"; + +const checkGlmPriceBeforeStarting: GolemPluginInitializer<{ + maxPrice: number; +}> = async (_glm, opts) => { + // Call an exchange to get the quotes + const response = await fetch("https://api.coinpaprika.com/v1/tickers/glm-golem"); + if (!response.ok) { + throw new Error("Failed to fetch GLM price"); + } else { + // Execute your logic + const data = await response.json(); + const price = parseFloat(data.quotes.USD.price); + + console.log("=== GLM Price ==="); + console.log("GLM price is", price); + console.log("=== GLM Price ==="); + if (price > opts.maxPrice) { + // Throwing inside the plugin will make `connect` throw, and then prevent + // execution of the rest of the script + throw new Error("GLM price is too high, won't compute today :O"); + } + } +}; +``` + +Here's how to use the plugin: + +```ts +import { GolemNetwork } from "./golem-network"; + +const glm = new GolemNetwork(); +glm.use(checkGlmPriceBeforeStarting, { + maxPrice: 0.5, +}); +``` + +## Plugin lifecycle and cleanup + +When you register plugins using `GolemNetwork.use`, they are initialized during the `GolemNetwork.connect` call. If any +plugin throws an error during initialization, the entire `connect` method will throw an error. + +Each plugin can return a cleanup function, which will be executed during `GolemNetwork.disconnect`. This is particularly +useful for plugins that allocate resources, use timers, or open database connections. + +## Synchronous and asynchronous plugins + +The SDK supports both synchronous and asynchronous plugins. You can register all of them using the `GolemNetwork.use` +method, and they will be initialized sequentially in the order they were registered. + +We hope you find this guide helpful and enjoy contributing to the Golem Network! Happy coding! diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts index aa7750617..d1be79a87 100644 --- a/src/golem-network/golem-network.ts +++ b/src/golem-network/golem-network.ts @@ -36,6 +36,7 @@ import { NetworkApiAdapter } from "../shared/yagna/adapters/network-api-adapter" import { IProposalRepository } from "../market/proposal"; import { Subscription } from "rxjs"; import { GolemConfigError } from "../shared/error/golem-error"; +import { GolemPluginInitializer, GolemPluginOptions, GolemPluginRegistration } from "./plugin"; /** * Instance of an object or a factory function that you can call `new` on. @@ -222,6 +223,8 @@ export class GolemNetwork { */ private cleanupTasks: (() => Promise | void)[] = []; + private registeredPlugins: GolemPluginRegistration[] = []; + constructor(options: Partial = {}) { const optDefaults: GolemNetworkOptions = { dataTransferProtocol: "ws", @@ -320,6 +323,7 @@ export class GolemNetwork { await this.yagna.connect(); await this.services.paymentApi.connect(); await this.storageProvider.init(); + await this.connectPlugins(); this.events.emit("connected"); this.hasConnection = true; } catch (err) { @@ -648,6 +652,21 @@ export class GolemNetwork { return await this.network.removeNetwork(network); } + public use(pluginCallback: GolemPluginInitializer): void; + public use( + pluginCallback: GolemPluginInitializer, + pluginOptions: TPOptions, + ): void; + public use( + pluginCallback: GolemPluginInitializer, + pluginOptions?: TPOptions, + ): void { + this.registeredPlugins.push({ + initializer: pluginCallback, + options: pluginOptions, + }); + } + private createStorageProvider(): StorageProvider { if (typeof this.options.dataTransferProtocol === "string") { switch (this.options.dataTransferProtocol) { @@ -668,4 +687,15 @@ export class GolemNetwork { return new NullStorageProvider(); } } + + private async connectPlugins() { + this.logger.debug("Started plugin initialization"); + for (const plugin of this.registeredPlugins) { + const cleanup = await plugin.initializer(this, plugin.options); + if (cleanup) { + this.cleanupTasks.push(cleanup); + } + } + this.logger.debug("Finished plugin initialization"); + } } diff --git a/src/golem-network/index.ts b/src/golem-network/index.ts index 2a17939a9..13cf43d54 100644 --- a/src/golem-network/index.ts +++ b/src/golem-network/index.ts @@ -1 +1,2 @@ export * from "./golem-network"; +export { GolemPluginInitializer, GolemPluginOptions, GolemPluginDisconnect } from "./plugin"; diff --git a/src/golem-network/plugin.ts b/src/golem-network/plugin.ts new file mode 100644 index 000000000..3652f7234 --- /dev/null +++ b/src/golem-network/plugin.ts @@ -0,0 +1,38 @@ +import { GolemNetwork } from "./golem-network"; + +/** + * Represents a generic cleanup task function that will be executed when the plugin lifetime reaches its end + */ +export type GolemPluginDisconnect = () => Promise | void; + +/** + * Generic type for plugin options + * + * Plugin options are optional by design and plugin developers should use this type when they + * want to enforce type safety on their plugin usage + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type GolemPluginOptions = Record | undefined; + +/** + * A generic plugin registration/connect function + * + * Golem plugins are initialized during {@link GolemNetwork.connect} + * + * A plugin initializer may return a cleanup function which will be called udring {@link GolemNetwork.disconnect} + */ +export type GolemPluginInitializer = ( + glm: GolemNetwork, + options: T, +) => void | GolemPluginDisconnect | Promise; + +/** + * Internal data structure that allows deferring plugin initialization to the `connect` call + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type GolemPluginRegistration = { + /** Plugin initialization function */ + initializer: GolemPluginInitializer; + /** Options to pass to the initialization function when it's executed */ + options?: T; +};