diff --git a/packages/automated-dispute/src/eboActor.ts b/packages/automated-dispute/src/eboActor.ts index 8be489a..d7189ae 100644 --- a/packages/automated-dispute/src/eboActor.ts +++ b/packages/automated-dispute/src/eboActor.ts @@ -42,6 +42,15 @@ export class EboActor { private readonly logger: ILogger, ) {} + /** + * Get the request ID this actor is handling. + * + * @returns request ID + */ + public getRequestId(): string { + return this.actorRequest.id; + } + /** * Handle `RequestCreated` event. * diff --git a/packages/automated-dispute/src/eboActorsManager.ts b/packages/automated-dispute/src/eboActorsManager.ts new file mode 100644 index 0000000..fa4fe75 --- /dev/null +++ b/packages/automated-dispute/src/eboActorsManager.ts @@ -0,0 +1,43 @@ +import { EboActor } from "./eboActor.js"; +import { RequestAlreadyHandled } from "./exceptions/index.js"; + +export class EboActorsManager { + private readonly requestActorMap: Map; + + constructor() { + this.requestActorMap = new Map(); + } + + /** + * Registers the actor and makes it fetchable by the ID of the request the actor is handling. + * + * @param actor an `EboActor` instance that handles the request + */ + public registerActor(actor: EboActor): void { + const requestId = actor.getRequestId(); + + if (this.requestActorMap.has(requestId)) throw new RequestAlreadyHandled(requestId); + + this.requestActorMap.set(requestId, actor); + } + + /** + * Get the `EboActor` instance linked with the `requestId`. + * + * @param requestId request ID + * @returns an `EboActor` instance if found by `requestId`, otherwise `undefined` + */ + public getActor(requestId: string): EboActor | undefined { + return this.requestActorMap.get(requestId); + } + + /** + * Deletes an actor from the manager, based on its linked request. + * + * @param requestId request ID + * @returns `true` if there was a linked actor for the request ID and it was removed, or `false` if the request was not linked to any actor. + */ + public deleteActor(requestId: string): boolean { + return this.requestActorMap.delete(requestId); + } +} diff --git a/packages/automated-dispute/src/exceptions/index.ts b/packages/automated-dispute/src/exceptions/index.ts index 4050512..a0a14e0 100644 --- a/packages/automated-dispute/src/exceptions/index.ts +++ b/packages/automated-dispute/src/exceptions/index.ts @@ -1,2 +1,4 @@ export * from "./rpcUrlsEmpty.exception.js"; export * from "./invalidActorState.exception.js"; +export * from "./requestAlreadyHandled.exception.js"; +export * from "./requestMismatch.js"; diff --git a/packages/automated-dispute/src/exceptions/requestAlreadyHandled.exception.ts b/packages/automated-dispute/src/exceptions/requestAlreadyHandled.exception.ts new file mode 100644 index 0000000..d3fb0b7 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/requestAlreadyHandled.exception.ts @@ -0,0 +1,7 @@ +export class RequestAlreadyHandled extends Error { + constructor(requestId: string) { + super(`Request ${requestId} is already being handled by another actor.`); + + this.name = "RequestAlreadyHandled"; + } +} diff --git a/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts b/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts index 5303301..fc58645 100644 --- a/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onRequestCreated.spec.ts @@ -6,21 +6,16 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { EboActor } from "../../src/eboActor.js"; import { EboMemoryRegistry } from "../../src/eboMemoryRegistry.js"; -import { RequestMismatch } from "../../src/exceptions/requestMismatch.js"; +import { RequestMismatch } from "../../src/exceptions/index.js"; import { ProtocolProvider } from "../../src/protocolProvider.js"; -import { EboEvent } from "../../src/types/events.js"; -import { Response } from "../../src/types/prophet.js"; +import { EboEvent, Response } from "../../src/types/index.js"; +import mocks from "../mocks/index.js"; import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, DEFAULT_MOCKED_REQUEST_CREATED_DATA, } from "./fixtures.js"; -const logger: ILogger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), -}; +const logger: ILogger = mocks.mockLogger(); describe("EboActor", () => { describe("onRequestCreated", () => { diff --git a/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts b/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts index 0c40792..6955743 100644 --- a/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onRequestFinalized.spec.ts @@ -3,15 +3,10 @@ import { describe, expect, it, vi } from "vitest"; import { InvalidActorState } from "../../src/exceptions/invalidActorState.exception.js"; import { EboEvent } from "../../src/types/events.js"; +import mocks from "../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.js"; -import mocks from "./mocks/index.js"; -const logger: ILogger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), -}; +const logger: ILogger = mocks.mockLogger(); describe("EboActor", () => { describe("onRequestFinalized", () => { diff --git a/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts b/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts index 4246348..5c66e78 100644 --- a/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onResponseDisputed.spec.ts @@ -5,15 +5,10 @@ import { describe, expect, it, vi } from "vitest"; import { InvalidActorState } from "../../src/exceptions/invalidActorState.exception"; import { EboEvent } from "../../src/types/events"; import { Response } from "../../src/types/prophet"; +import mocks from "../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures"; -import mocks from "./mocks/index.js"; - -const logger: ILogger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), -}; + +const logger: ILogger = mocks.mockLogger(); describe("onResponseDisputed", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; diff --git a/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts b/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts index 6ebac86..25bbc99 100644 --- a/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts +++ b/packages/automated-dispute/tests/eboActor/onResponseProposed.spec.ts @@ -3,15 +3,10 @@ import { describe, expect, it, vi } from "vitest"; import { InvalidActorState } from "../../src/exceptions/invalidActorState.exception"; import { EboEvent } from "../../src/types/events"; +import mocks from "../mocks/index.ts"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.ts"; -import mocks from "./mocks/index.ts"; - -const logger: ILogger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), -}; + +const logger: ILogger = mocks.mockLogger(); describe("onResponseProposed", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; diff --git a/packages/automated-dispute/tests/eboActorsManager.spec.ts b/packages/automated-dispute/tests/eboActorsManager.spec.ts new file mode 100644 index 0000000..a55c261 --- /dev/null +++ b/packages/automated-dispute/tests/eboActorsManager.spec.ts @@ -0,0 +1,80 @@ +import { ILogger } from "@ebo-agent/shared"; +import { describe, expect, it, vi } from "vitest"; + +import { EboActorsManager } from "../src/eboActorsManager.js"; +import { RequestAlreadyHandled } from "../src/exceptions/index.js"; +import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./eboActor/fixtures.js"; +import mocks from "./mocks/index.js"; + +const logger: ILogger = mocks.mockLogger(); + +describe("EboActorsManager", () => { + describe("registerActor", () => { + it("registers the actor correctly", () => { + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const { actor } = mocks.buildEboActor(request, logger); + const actorsManager = new EboActorsManager(); + const mockSetRequestActorMap = vi.spyOn(actorsManager["requestActorMap"], "set"); + + actorsManager.registerActor(actor); + + expect(mockSetRequestActorMap).toHaveBeenCalledWith(request.id, actor); + expect(actorsManager.getActor(actor.getRequestId())).toBe(actor); + }); + + it("throws if the request has already an actor linked to it", () => { + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const { actor: firstActor } = mocks.buildEboActor(request, logger); + const { actor: secondActor } = mocks.buildEboActor(request, logger); + const actorsManager = new EboActorsManager(); + + actorsManager.registerActor(firstActor); + + expect(() => actorsManager.registerActor(secondActor)).toThrowError( + RequestAlreadyHandled, + ); + }); + }); + + describe("getActor", () => { + it("returns undefined if the request is not linked to any actor", () => { + const actorsManager = new EboActorsManager(); + + expect(actorsManager.getActor("0x9999")).toBeUndefined(); + }); + + it("returns the request's linked actor", () => { + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const { actor } = mocks.buildEboActor(request, logger); + const actorsManager = new EboActorsManager(); + + actorsManager.registerActor(actor); + + expect(actorsManager.getActor(request.id)).toBe(actor); + }); + }); + + describe("deleteActor", () => { + it("deletes the actor linked to the request", () => { + const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const { actor } = mocks.buildEboActor(request, logger); + const actorsManager = new EboActorsManager(); + + actorsManager.registerActor(actor); + + expect(actorsManager.getActor(request.id)).toBe(actor); + + actorsManager.deleteActor(request.id); + + expect(actorsManager.getActor(request.id)).toBeUndefined(); + }); + + it("returns false if the request has no actors linked", () => { + const requestId = "0x01"; + const actorsManager = new EboActorsManager(); + + expect(actorsManager.getActor(requestId)).toBeUndefined(); + expect(actorsManager.deleteActor(requestId)).toEqual(false); + }); + }); +}); diff --git a/packages/automated-dispute/tests/eboActor/mocks/index.ts b/packages/automated-dispute/tests/mocks/eboActor.ts similarity index 78% rename from packages/automated-dispute/tests/eboActor/mocks/index.ts rename to packages/automated-dispute/tests/mocks/eboActor.ts index 47d5039..b18b857 100644 --- a/packages/automated-dispute/tests/eboActor/mocks/index.ts +++ b/packages/automated-dispute/tests/mocks/eboActor.ts @@ -3,11 +3,11 @@ import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types"; import { ILogger } from "@ebo-agent/shared"; import { vi } from "vitest"; -import { EboActor } from "../../../src/eboActor"; -import { EboMemoryRegistry } from "../../../src/eboMemoryRegistry"; -import { ProtocolProvider } from "../../../src/protocolProvider"; -import { Request, Response } from "../../../src/types/prophet"; -import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS } from "../fixtures"; +import { EboActor } from "../../src/eboActor"; +import { EboMemoryRegistry } from "../../src/eboMemoryRegistry"; +import { ProtocolProvider } from "../../src/protocolProvider"; +import { Request, Response } from "../../src/types/prophet"; +import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS } from "../eboActor/fixtures"; /** * Builds a base `EboActor` scaffolded with all its dependencies. @@ -16,7 +16,7 @@ import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS } from "../fixtures"; * @param logger logger * @returns */ -function buildEboActor(request: Request, logger: ILogger) { +export function buildEboActor(request: Request, logger: ILogger) { const { id, chainId, epoch, epochTimestamp } = request; const onTerminate = vi.fn(); @@ -60,7 +60,7 @@ function buildEboActor(request: Request, logger: ILogger) { * @param attributes custom attributes to set into the response to build * @returns a `Response` */ -function buildResponse(request: Request, attributes: Partial = {}): Response { +export function buildResponse(request: Request, attributes: Partial = {}): Response { const baseResponse: Response = { id: "0x01", wasDisputed: false, @@ -80,5 +80,3 @@ function buildResponse(request: Request, attributes: Partial = {}): Re ...attributes, }; } - -export default { buildEboActor, buildResponse }; diff --git a/packages/automated-dispute/tests/mocks/index.ts b/packages/automated-dispute/tests/mocks/index.ts new file mode 100644 index 0000000..0aa2842 --- /dev/null +++ b/packages/automated-dispute/tests/mocks/index.ts @@ -0,0 +1,4 @@ +import { buildEboActor, buildResponse } from "./eboActor.js"; +import { mockLogger } from "./logger.js"; + +export default { buildEboActor, buildResponse, mockLogger }; diff --git a/packages/automated-dispute/tests/mocks/logger.ts b/packages/automated-dispute/tests/mocks/logger.ts new file mode 100644 index 0000000..18743c7 --- /dev/null +++ b/packages/automated-dispute/tests/mocks/logger.ts @@ -0,0 +1,9 @@ +import { ILogger } from "@ebo-agent/shared"; +import { vi } from "vitest"; + +export const mockLogger: () => ILogger = () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), +});