From 55c8d752a7e3df14ea2e7da1c28d9717c7b1459d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Pi=C4=85tkowski?= Date: Thu, 4 Dec 2025 00:34:54 +0100 Subject: [PATCH 1/3] feat: persist shareable state when leader reconnects --- fe/src/App.tsx | 1 - .../JoinLockedBox/WorkflowManager.ts | 267 ++++++++++++++++++ .../JoinLockedBox/keySharingWorkflow.ts | 59 ++++ .../useJoinLockedBoxConnection.ts | 6 + fe/src/components/RootRoutes.tsx | 2 - 5 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/WorkflowManager.ts create mode 100644 fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts diff --git a/fe/src/App.tsx b/fe/src/App.tsx index 01b4ef5..cd410d1 100644 --- a/fe/src/App.tsx +++ b/fe/src/App.tsx @@ -29,7 +29,6 @@ const LazyRootRoutes = lazy(() => function App() { const theme = useSystemTheme(); - console.log("App"); return ( = (data: T) => boolean; +type EventHandler> = (data: T, workflow: WorkflowDefinition, proto: TProto) => void | Promise; + +interface WorkflowStep { + event: EventPredicate; + condition?: () => boolean; + handler?: EventHandler; +} + +interface WorkflowDefinition { + name: string; + steps: WorkflowStep[]; + onComplete?: () => void | Promise; + onError?: (error: Error) => void; +} + +interface WorkflowState { + currentStepIndex: number; + isActive: boolean; + data: Record; +} + +export class WorkflowManager< + TMessage extends Record = Record, + TProto extends BasicProtoInterface = BasicProtoInterface, +> { + private workflows: Map> = new Map(); + private workflowStates: Map = new Map(); + private eventBuffer: Array<{ peerId: string; message: TMessage }> = []; + private isProcessing = false; + private lastReferencedProto: TProto | undefined; + readonly router: RouterItem; + + constructor(router?: PeersMessageRouter) { + this.router = (peerId, message) => { + this.handleIncomingMessage(peerId, message); + }; + + if (router) { + this.attachToRouter(router); + } + } + + /** + * Attach the workflow manager to a message router + */ + attachToRouter(router: PeersMessageRouter) { + // Create a universal handler that captures all messages + router.addHandler((_msg): _msg is TMessage => true, (peerId, message, proto) => { + this.lastReferencedProto = proto; + this.handleIncomingMessage(peerId, message); + }); + } + + /** + * Define a new workflow + */ + defineWorkflow(workflow: WorkflowDefinition): void { + this.workflows.set(workflow.name, workflow); + this.workflowStates.set(workflow.name, { + currentStepIndex: 0, + isActive: false, + data: {}, + }); + } + + /** + * Start a workflow + */ + startWorkflow(name: string, initialData?: Record): void { + const workflow = this.workflows.get(name); + if (!workflow) { + throw new Error(`Workflow "${name}" not found`); + } + + const state = this.workflowStates.get(name); + if (!state) { + throw new Error(`Workflow state for "${name}" not found`); + } + + state.isActive = true; + state.currentStepIndex = 0; + state.data = { ...initialData }; + + // Process any buffered events + this.processEventBuffer(); + } + + /** + * Stop a workflow + */ + stopWorkflow(name: string): void { + const state = this.workflowStates.get(name); + if (state) { + state.isActive = false; + state.currentStepIndex = 0; + state.data = {}; + } + } + + /** + * Reset all workflows + */ + reset(): void { + this.workflowStates.forEach((state) => { + state.isActive = false; + state.currentStepIndex = 0; + state.data = {}; + }); + this.eventBuffer = []; + } + + /** + * Get workflow state + */ + getWorkflowState(name: string): WorkflowState | undefined { + return this.workflowStates.get(name); + } + + /** + * Update workflow data + */ + updateWorkflowData(name: string, data: Record): void { + const state = this.workflowStates.get(name); + if (state) { + state.data = { ...state.data, ...data }; + } + } + + /** + * Handle incoming messages from router + */ + private handleIncomingMessage(peerId: string, message: TMessage): void { + // Buffer the event + this.eventBuffer.push({ peerId, message }); + + // Process if not already processing + if (!this.isProcessing) { + this.processEventBuffer(); + } + } + + /** + * Process buffered events + */ + private async processEventBuffer(): Promise { + if (this.isProcessing || this.eventBuffer.length === 0) { + return; + } + + this.isProcessing = true; + + while (this.eventBuffer.length > 0) { + const event = this.eventBuffer.shift(); + if (!event) continue; + + await this.processEvent(event.peerId, event.message); + } + + this.isProcessing = false; + } + + /** + * Process a single event against all active workflows + */ + private async processEvent(peerId: string, message: TMessage): Promise { + for (const [workflowName, workflow] of this.workflows.entries()) { + const state = this.workflowStates.get(workflowName); + if (!state || !state.isActive) continue; + + const currentStep = workflow.steps[state.currentStepIndex]; + if (!currentStep) continue; + + try { + if (!currentStep.event(message)) continue; + + if (currentStep.condition && !currentStep.condition()) continue; + + state.data._lastPeerId = peerId; + state.data._lastMessage = message; + + if (currentStep.handler) { + await currentStep.handler(message, workflow, this.lastReferencedProto!); + } + + state.currentStepIndex++; + + if (state.currentStepIndex >= workflow.steps.length) { + state.isActive = false; + state.currentStepIndex = 0; + } + } catch (error) { + if (workflow.onError) { + workflow.onError(error as Error); + } + + state.isActive = false; + state.currentStepIndex = 0; + state.data = {}; + } + } + } + +} + +/** + * Helper class for building workflows with a fluent API + */ +export class WorkflowBuilder< + TMessage extends Record = Record, +> { + private workflow: Partial> = { + steps: [], + }; + + constructor(name: string) { + this.workflow.name = name; + } + + /** + * Add a step that waits for an event + */ + waitFor( + event: EventPredicate, + handler?: EventHandler, + ): WorkflowBuilder { + this.workflow.steps!.push({ + event: event as EventPredicate, + handler: handler as EventHandler | undefined, + }); + return this; + } + + /** + * Add a step with a condition + */ + waitForWithCondition( + event: EventPredicate, + condition: () => boolean, + handler?: EventHandler, + ): WorkflowBuilder { + this.workflow.steps!.push({ + event: event as EventPredicate, + condition, + handler: handler as EventHandler | undefined, + }); + return this; + } + + /** + * Set error handler + */ + onError(handler: (error: Error) => void): WorkflowBuilder { + this.workflow.onError = handler; + return this; + } + + public build(): WorkflowDefinition { + return this.workflow as WorkflowDefinition; + } +} diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts new file mode 100644 index 0000000..dd542f8 --- /dev/null +++ b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts @@ -0,0 +1,59 @@ +import { useJoinLockedBoxStore } from "@/stores/boxStore/joinLockedBoxStore"; +import { + type FollowerSendsPartialStateMessage, + isLeaderSendsPartialStateMessage, + isLeaderWelcome, + type LeaderSendsPartialStateMessage, +} from "../commons/leader-keyholder-interface"; +import { WorkflowBuilder, WorkflowManager } from "./WorkflowManager"; + +export const keySharingWorkflowManager = new WorkflowManager(); + +const createKeySharingWorkflow = () => { + return new WorkflowBuilder("keySharing") + .waitForWithCondition(isLeaderWelcome, () => { + debugger; + const { shareAccessKeyByKeyHolderId } = useJoinLockedBoxStore.getState(); + + const isThereAnyShared = Object.values(shareAccessKeyByKeyHolderId).some( + (x) => x === true, + ); + + return isThereAnyShared; + }) + .waitFor(isLeaderSendsPartialStateMessage, (message, _, proto) => { + debugger; + const { connectedLeaderId, shareAccessKeyByKeyHolderId } = + useJoinLockedBoxStore.getState(); + + if (!connectedLeaderId) { + return; + } + + const { onlineKeyHolders } = message as LeaderSendsPartialStateMessage; + const onlineKeyHoldersIds = + onlineKeyHolders?.map((keyHolder) => keyHolder.id) ?? []; + + const keyHoldersIdsToSharedKeyWith = Object.entries( + shareAccessKeyByKeyHolderId, + ) + .filter( + ([keyHolderId, shared]) => + shared && onlineKeyHoldersIds.includes(keyHolderId), + ) + .map(([keyHolderId]) => keyHolderId); + + proto.sendMessageToPeer(connectedLeaderId, { + type: "follower:send-partial-state", + keyHoldersIdsToSharedKeyWith, + } satisfies FollowerSendsPartialStateMessage); + }) + .onError((error) => { + console.error("Key sharing workflow error:", error); + + const state = useJoinLockedBoxStore.getState(); + state.actions.setError(`Workflow error: ${error.message}`); + }); +}; + +keySharingWorkflowManager.defineWorkflow(createKeySharingWorkflow().build()); diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts index 8918eb9..5c71439 100644 --- a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts +++ b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts @@ -4,6 +4,7 @@ import { useJoinLockedBoxStore } from "@/stores/boxStore/joinLockedBoxStore"; import { usePeerToHolderMapRef } from "../commons/usePeerToHolderMapRef"; import { router } from "./dataChannelRouter"; import { useDataChannelSendMessages } from "./dataChannelSendMessages"; +import { keySharingWorkflowManager } from "./keySharingWorkflow"; import { useOnChangeShareablePartOfState } from "./useSelectiveStatePusher"; export type JoinBoxConnectionError = ReturnType< @@ -28,9 +29,14 @@ export function useJoinLockedBoxConnection({ useEffect(() => { routerMng.addRouter("join-unlock-box", router.router); + routerMng.addRouter( + "join-unlock-box-workflows", + keySharingWorkflowManager.router, + ); return () => { routerMng.removeRouter("join-unlock-box"); + routerMng.removeRouter("join-unlock-box-workflows"); }; }, [routerMng]); diff --git a/fe/src/components/RootRoutes.tsx b/fe/src/components/RootRoutes.tsx index f709885..08bd33a 100644 --- a/fe/src/components/RootRoutes.tsx +++ b/fe/src/components/RootRoutes.tsx @@ -10,7 +10,6 @@ import { MainLayout } from "./layout"; import { FullPageLoader, Loader } from "./ui"; const Root: FC = () => { - console.log("Root component rendered"); return ( <> {window.icod2Dev.topNavTools.get() === true && ( @@ -113,6 +112,5 @@ const router = createBrowserRouter([ ]); export const RootRoutes = () => { - console.log("RootRoutes component rendered"); return ; }; From 1ca0eb9ac44e6bfae40ca436acf4a5b047d08135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Pi=C4=85tkowski?= Date: Thu, 4 Dec 2025 11:33:59 +0100 Subject: [PATCH 2/3] fix: improve sharing key state after leader gets connected --- .../JoinLockedBox/JoinLockedBox.tsx | 11 +- .../JoinLockedBox/keySharingWorkflow.ts | 18 +- .../useJoinLockedBoxConnection.ts | 4 +- .../libp2p/workflows/WorkflowBuilder.ts | 47 ++++++ .../libp2p/workflows}/WorkflowManager.ts | 156 +++--------------- fe/src/services/libp2p/workflows/index.ts | 3 + fe/src/services/libp2p/workflows/types.ts | 27 +++ 7 files changed, 125 insertions(+), 141 deletions(-) create mode 100644 fe/src/services/libp2p/workflows/WorkflowBuilder.ts rename fe/src/{components/Box/sub-pages/RestoreBoxes/JoinLockedBox => services/libp2p/workflows}/WorkflowManager.ts (51%) create mode 100644 fe/src/services/libp2p/workflows/index.ts create mode 100644 fe/src/services/libp2p/workflows/types.ts diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/JoinLockedBox.tsx b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/JoinLockedBox.tsx index 9beb34c..2a2f4c7 100644 --- a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/JoinLockedBox.tsx +++ b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/JoinLockedBox.tsx @@ -1,4 +1,4 @@ -import { type FC, useMemo } from "react"; +import { type FC, useEffect, useMemo } from "react"; import { PortalPeerId } from "@/components/Box/components/PortalPeerId"; import { RelayReconnectingAlert } from "@/components/Box/components/RelayReconnectingAlert"; import { ShareAccessButton as ShareAccessButtonDumb } from "@/components/Box/components/ShareAccessButton"; @@ -19,6 +19,7 @@ import { NavigationAwayBlocker } from "../commons/components/NavigationAwayBlock import { PageTitle } from "../commons/components/PageTitle"; import { useDataChannelSendMessages } from "./dataChannelSendMessages"; import { useSendKeyToLeader } from "./hooks"; +import { keySharingWorkflowManager } from "./keySharingWorkflow"; import { useJoinLockedBoxConnection } from "./useJoinLockedBoxConnection"; export const JoinLockedBox: FC = () => { @@ -48,6 +49,14 @@ export const JoinLockedBox: FC = () => { }; const JoinLockedBoxContent: React.FC = () => { + useEffect(() => { + keySharingWorkflowManager.startAllWorkflows(); + + return () => { + keySharingWorkflowManager.reset(); + }; + }, []); + const state = useJoinLockedBoxStore((state) => state.state); const roomToken = useJoinLockedBoxStore((state) => state.roomToken); diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts index dd542f8..d9a05c6 100644 --- a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts +++ b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/keySharingWorkflow.ts @@ -1,3 +1,5 @@ +import { loggerGate } from "@icod2/protocols"; +import { WorkflowBuilder, WorkflowManager } from "@/services/libp2p/workflows"; import { useJoinLockedBoxStore } from "@/stores/boxStore/joinLockedBoxStore"; import { type FollowerSendsPartialStateMessage, @@ -5,14 +7,13 @@ import { isLeaderWelcome, type LeaderSendsPartialStateMessage, } from "../commons/leader-keyholder-interface"; -import { WorkflowBuilder, WorkflowManager } from "./WorkflowManager"; +import { usePeerToHolderMapRef } from "../commons/usePeerToHolderMapRef"; export const keySharingWorkflowManager = new WorkflowManager(); const createKeySharingWorkflow = () => { return new WorkflowBuilder("keySharing") .waitForWithCondition(isLeaderWelcome, () => { - debugger; const { shareAccessKeyByKeyHolderId } = useJoinLockedBoxStore.getState(); const isThereAnyShared = Object.values(shareAccessKeyByKeyHolderId).some( @@ -22,7 +23,6 @@ const createKeySharingWorkflow = () => { return isThereAnyShared; }) .waitFor(isLeaderSendsPartialStateMessage, (message, _, proto) => { - debugger; const { connectedLeaderId, shareAccessKeyByKeyHolderId } = useJoinLockedBoxStore.getState(); @@ -43,7 +43,16 @@ const createKeySharingWorkflow = () => { ) .map(([keyHolderId]) => keyHolderId); - proto.sendMessageToPeer(connectedLeaderId, { + const leaderPeerId = usePeerToHolderMapRef + .getValue() + .getPeerId(connectedLeaderId); + + if (!leaderPeerId) { + loggerGate.canError && console.error("Leader peer ID not found"); + return; + } + + proto.sendMessageToPeer(leaderPeerId, { type: "follower:send-partial-state", keyHoldersIdsToSharedKeyWith, } satisfies FollowerSendsPartialStateMessage); @@ -57,3 +66,4 @@ const createKeySharingWorkflow = () => { }; keySharingWorkflowManager.defineWorkflow(createKeySharingWorkflow().build()); +keySharingWorkflowManager.startWorkflow("keySharing"); diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts index 5c71439..09679af 100644 --- a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts +++ b/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/useJoinLockedBoxConnection.ts @@ -34,11 +34,13 @@ export function useJoinLockedBoxConnection({ keySharingWorkflowManager.router, ); + console.log("useEffect in useJoinLockedBoxConnection"); + return () => { routerMng.removeRouter("join-unlock-box"); routerMng.removeRouter("join-unlock-box-workflows"); }; - }, [routerMng]); + }, [routerMng.addRouter, routerMng.removeRouter]); useEffect(() => { useJoinLockedBoxStore diff --git a/fe/src/services/libp2p/workflows/WorkflowBuilder.ts b/fe/src/services/libp2p/workflows/WorkflowBuilder.ts new file mode 100644 index 0000000..194d3b7 --- /dev/null +++ b/fe/src/services/libp2p/workflows/WorkflowBuilder.ts @@ -0,0 +1,47 @@ +import type { EventHandler, EventPredicate, WorkflowDefinition } from "./types"; + +export class WorkflowBuilder< + TMessage extends Record = Record, +> { + private workflow: Pick, "steps"> & + Partial, "steps">> = { + steps: [], + }; + + constructor(name: string) { + this.workflow.name = name; + } + + waitFor( + event: EventPredicate, + handler?: EventHandler, + ): WorkflowBuilder { + this.workflow.steps.push({ + event: event as EventPredicate, + handler: handler as EventHandler | undefined, + }); + return this; + } + + waitForWithCondition( + event: EventPredicate, + condition: () => boolean, + handler?: EventHandler, + ): WorkflowBuilder { + this.workflow.steps.push({ + event: event as EventPredicate, + condition, + handler: handler as EventHandler | undefined, + }); + return this; + } + + onError(handler: (error: Error) => void): WorkflowBuilder { + this.workflow.onError = handler; + return this; + } + + public build(): WorkflowDefinition { + return this.workflow as WorkflowDefinition; + } +} diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/WorkflowManager.ts b/fe/src/services/libp2p/workflows/WorkflowManager.ts similarity index 51% rename from fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/WorkflowManager.ts rename to fe/src/services/libp2p/workflows/WorkflowManager.ts index 407c9cf..3b02d17 100644 --- a/fe/src/components/Box/sub-pages/RestoreBoxes/JoinLockedBox/WorkflowManager.ts +++ b/fe/src/services/libp2p/workflows/WorkflowManager.ts @@ -1,30 +1,5 @@ -import type { - BasicProtoInterface, - PeersMessageRouter, - RouterItem, -} from "@/services/libp2p"; - -type EventPredicate = (data: T) => boolean; -type EventHandler> = (data: T, workflow: WorkflowDefinition, proto: TProto) => void | Promise; - -interface WorkflowStep { - event: EventPredicate; - condition?: () => boolean; - handler?: EventHandler; -} - -interface WorkflowDefinition { - name: string; - steps: WorkflowStep[]; - onComplete?: () => void | Promise; - onError?: (error: Error) => void; -} - -interface WorkflowState { - currentStepIndex: number; - isActive: boolean; - data: Record; -} +import type { BasicProtoInterface, RouterItem } from "@/services/libp2p"; +import type { WorkflowDefinition, WorkflowState } from "./types"; export class WorkflowManager< TMessage extends Record = Record, @@ -37,30 +12,13 @@ export class WorkflowManager< private lastReferencedProto: TProto | undefined; readonly router: RouterItem; - constructor(router?: PeersMessageRouter) { - this.router = (peerId, message) => { - this.handleIncomingMessage(peerId, message); - }; - - if (router) { - this.attachToRouter(router); - } - } - - /** - * Attach the workflow manager to a message router - */ - attachToRouter(router: PeersMessageRouter) { - // Create a universal handler that captures all messages - router.addHandler((_msg): _msg is TMessage => true, (peerId, message, proto) => { + constructor() { + this.router = (peerId, message, proto) => { this.lastReferencedProto = proto; this.handleIncomingMessage(peerId, message); - }); + }; } - /** - * Define a new workflow - */ defineWorkflow(workflow: WorkflowDefinition): void { this.workflows.set(workflow.name, workflow); this.workflowStates.set(workflow.name, { @@ -70,10 +28,7 @@ export class WorkflowManager< }); } - /** - * Start a workflow - */ - startWorkflow(name: string, initialData?: Record): void { + startWorkflow(name: string, initialData?: Record): void { const workflow = this.workflows.get(name); if (!workflow) { throw new Error(`Workflow "${name}" not found`); @@ -88,13 +43,17 @@ export class WorkflowManager< state.currentStepIndex = 0; state.data = { ...initialData }; - // Process any buffered events this.processEventBuffer(); } - /** - * Stop a workflow - */ + startAllWorkflows(): void { + this.workflowStates.forEach((state, name) => { + if (!state.isActive) { + this.startWorkflow(name); + } + }); + } + stopWorkflow(name: string): void { const state = this.workflowStates.get(name); if (state) { @@ -104,9 +63,6 @@ export class WorkflowManager< } } - /** - * Reset all workflows - */ reset(): void { this.workflowStates.forEach((state) => { state.isActive = false; @@ -116,39 +72,25 @@ export class WorkflowManager< this.eventBuffer = []; } - /** - * Get workflow state - */ getWorkflowState(name: string): WorkflowState | undefined { return this.workflowStates.get(name); } - /** - * Update workflow data - */ - updateWorkflowData(name: string, data: Record): void { + updateWorkflowData(name: string, data: Record): void { const state = this.workflowStates.get(name); if (state) { state.data = { ...state.data, ...data }; } } - /** - * Handle incoming messages from router - */ private handleIncomingMessage(peerId: string, message: TMessage): void { - // Buffer the event this.eventBuffer.push({ peerId, message }); - // Process if not already processing if (!this.isProcessing) { this.processEventBuffer(); } } - /** - * Process buffered events - */ private async processEventBuffer(): Promise { if (this.isProcessing || this.eventBuffer.length === 0) { return; @@ -166,9 +108,6 @@ export class WorkflowManager< this.isProcessing = false; } - /** - * Process a single event against all active workflows - */ private async processEvent(peerId: string, message: TMessage): Promise { for (const [workflowName, workflow] of this.workflows.entries()) { const state = this.workflowStates.get(workflowName); @@ -186,7 +125,12 @@ export class WorkflowManager< state.data._lastMessage = message; if (currentStep.handler) { - await currentStep.handler(message, workflow, this.lastReferencedProto!); + await currentStep.handler( + message, + workflow, + // biome-ignore lint/style/noNonNullAssertion: I'm positive it will always be not null + this.lastReferencedProto!, + ); } state.currentStepIndex++; @@ -206,62 +150,4 @@ export class WorkflowManager< } } } - -} - -/** - * Helper class for building workflows with a fluent API - */ -export class WorkflowBuilder< - TMessage extends Record = Record, -> { - private workflow: Partial> = { - steps: [], - }; - - constructor(name: string) { - this.workflow.name = name; - } - - /** - * Add a step that waits for an event - */ - waitFor( - event: EventPredicate, - handler?: EventHandler, - ): WorkflowBuilder { - this.workflow.steps!.push({ - event: event as EventPredicate, - handler: handler as EventHandler | undefined, - }); - return this; - } - - /** - * Add a step with a condition - */ - waitForWithCondition( - event: EventPredicate, - condition: () => boolean, - handler?: EventHandler, - ): WorkflowBuilder { - this.workflow.steps!.push({ - event: event as EventPredicate, - condition, - handler: handler as EventHandler | undefined, - }); - return this; - } - - /** - * Set error handler - */ - onError(handler: (error: Error) => void): WorkflowBuilder { - this.workflow.onError = handler; - return this; - } - - public build(): WorkflowDefinition { - return this.workflow as WorkflowDefinition; - } } diff --git a/fe/src/services/libp2p/workflows/index.ts b/fe/src/services/libp2p/workflows/index.ts new file mode 100644 index 0000000..3c6c483 --- /dev/null +++ b/fe/src/services/libp2p/workflows/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export { WorkflowBuilder } from "./WorkflowBuilder"; +export { WorkflowManager } from "./WorkflowManager"; diff --git a/fe/src/services/libp2p/workflows/types.ts b/fe/src/services/libp2p/workflows/types.ts new file mode 100644 index 0000000..0494205 --- /dev/null +++ b/fe/src/services/libp2p/workflows/types.ts @@ -0,0 +1,27 @@ +import type { BasicProtoInterface } from "../types"; + +export type EventPredicate = (data: T) => boolean; +export type EventHandler> = ( + data: T, + workflow: WorkflowDefinition, + proto: TProto, +) => void | Promise; + +export interface WorkflowStep { + event: EventPredicate; + condition?: () => boolean; + handler?: EventHandler; +} + +export interface WorkflowDefinition { + name: string; + steps: WorkflowStep[]; + onComplete?: () => void | Promise; + onError?: (error: Error) => void; +} + +export interface WorkflowState { + currentStepIndex: number; + isActive: boolean; + data: Record; +} From dda6f06b7c4554c0a42a97ba87ef83909a189ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Pi=C4=85tkowski?= Date: Thu, 4 Dec 2025 12:41:57 +0100 Subject: [PATCH 3/3] fix: improve in useEffect --- .../Box/sub-pages/RestoreBoxes/OpenLockedBox/OpenLockedBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fe/src/components/Box/sub-pages/RestoreBoxes/OpenLockedBox/OpenLockedBox.tsx b/fe/src/components/Box/sub-pages/RestoreBoxes/OpenLockedBox/OpenLockedBox.tsx index 43d2d74..91aca2b 100644 --- a/fe/src/components/Box/sub-pages/RestoreBoxes/OpenLockedBox/OpenLockedBox.tsx +++ b/fe/src/components/Box/sub-pages/RestoreBoxes/OpenLockedBox/OpenLockedBox.tsx @@ -73,7 +73,7 @@ export const OpenLockedBoxContent: FC = () => { return () => { routerMng.removeRouter("open-locked-box"); }; - }, [routerMng]); + }, [routerMng.addRouter, routerMng.removeRouter]); const { sendKey } = useSendMessageProto({ peerMessageProtoRef: messageProto.peerMessageProtoRef,