11import type { FlowcoreEvent } from "../../contracts/event.ts"
22import type { Logger } from "../logger.ts"
33import { NoopLogger } from "../logger.ts"
4+ import type { PumpState } from "../pump/types.ts"
45import type {
56 ClusterRole ,
67 ClusterSocket ,
@@ -11,6 +12,7 @@ import type {
1112 WsAckMessage ,
1213 WsFailMessage ,
1314 WsMessage ,
15+ WsResetMessage ,
1416} from "./types.ts"
1517import { createNodeTransport } from "./node-transport.ts"
1618
@@ -89,6 +91,8 @@ export class ClusterManager {
8991 private leaderConnection : ClusterSocket | null = null
9092 private eventHandler : ( ( pathway : string , event : FlowcoreEvent ) => Promise < void > ) | null = null
9193 private leadershipChangeHandler : ( ( isLeader : boolean ) => void ) | null = null
94+ private resetHandler : ( ( position ?: PumpState ) => Promise < void > ) | null = null
95+ private pendingResets : Map < string , { resolve : ( ) => void ; reject : ( error : Error ) => void ; sentAt : number } > = new Map ( )
9296
9397 constructor ( options : PathwayClusterOptions , logger ?: Logger ) {
9498 this . coordinator = options . coordinator
@@ -120,6 +124,50 @@ export class ClusterManager {
120124 this . leadershipChangeHandler = handler
121125 }
122126
127+ /**
128+ * Set a callback that handles pump reset requests on the leader.
129+ */
130+ onReset ( handler : ( position ?: PumpState ) => Promise < void > ) {
131+ this . resetHandler = handler
132+ }
133+
134+ /**
135+ * Request a pump reset. Routes to the leader automatically.
136+ * - If this instance is the leader: executes the reset directly.
137+ * - If this instance is a worker: forwards the request to the leader via WebSocket.
138+ */
139+ async requestReset ( position ?: PumpState ) : Promise < void > {
140+ if ( this . role === "unknown" ) {
141+ throw new Error ( "Cluster role not yet established — cannot reset" )
142+ }
143+
144+ if ( this . role === "leader" ) {
145+ if ( ! this . resetHandler ) {
146+ throw new Error ( "No reset handler set on ClusterManager" )
147+ }
148+ await this . resetHandler ( position )
149+ return
150+ }
151+
152+ // Worker: forward to leader via WS
153+ if ( ! this . leaderConnection || this . leaderConnection . readyState !== WebSocket . OPEN ) {
154+ throw new Error ( "Not connected to leader — cannot forward reset" )
155+ }
156+
157+ const resetId = crypto . randomUUID ( )
158+ return new Promise < void > ( ( resolve , reject ) => {
159+ this . pendingResets . set ( resetId , { resolve, reject, sentAt : Date . now ( ) } )
160+
161+ const msg : WsResetMessage = { type : "reset" , resetId, position }
162+ try {
163+ this . leaderConnection ! . send ( JSON . stringify ( msg ) )
164+ } catch ( err ) {
165+ this . pendingResets . delete ( resetId )
166+ reject ( err instanceof Error ? err : new Error ( String ( err ) ) )
167+ }
168+ } )
169+ }
170+
123171 /**
124172 * Start the cluster: register instance, begin heartbeat, attempt leader election
125173 */
@@ -222,6 +270,12 @@ export class ClusterManager {
222270 }
223271 this . pendingDeliveries . clear ( )
224272
273+ // Reject pending resets
274+ for ( const [ , pending ] of this . pendingResets ) {
275+ pending . reject ( new Error ( "Cluster manager stopped" ) )
276+ }
277+ this . pendingResets . clear ( )
278+
225279 // Unregister
226280 try {
227281 await this . coordinator . unregister ( this . instanceId )
@@ -445,6 +499,22 @@ export class ClusterManager {
445499 socket . send ( JSON . stringify ( { type : "pong" } ) )
446500 break
447501 }
502+ case "reset-ack" : {
503+ const pending = this . pendingResets . get ( msg . resetId )
504+ if ( pending ) {
505+ this . pendingResets . delete ( msg . resetId )
506+ pending . resolve ( )
507+ }
508+ break
509+ }
510+ case "reset-fail" : {
511+ const pending = this . pendingResets . get ( msg . resetId )
512+ if ( pending ) {
513+ this . pendingResets . delete ( msg . resetId )
514+ pending . reject ( new Error ( msg . error ) )
515+ }
516+ break
517+ }
448518 default :
449519 break
450520 }
@@ -461,10 +531,10 @@ export class ClusterManager {
461531 this . workerConnections . set ( address , ws )
462532 }
463533
464- ws . onmessage = ( event : { data : string } ) => {
534+ ws . onmessage = async ( event : { data : string } ) => {
465535 try {
466536 const msg : WsMessage = JSON . parse ( event . data )
467- this . handleLeaderMessage ( address , msg )
537+ await this . handleLeaderMessage ( address , msg )
468538 } catch ( err ) {
469539 this . logger . error (
470540 "Error handling worker response" ,
@@ -492,7 +562,7 @@ export class ClusterManager {
492562 }
493563 }
494564
495- private handleLeaderMessage ( workerAddress : string , msg : WsMessage ) : void {
565+ private async handleLeaderMessage ( workerAddress : string , msg : WsMessage ) : Promise < void > {
496566 switch ( msg . type ) {
497567 case "ack" : {
498568 const delivery = this . pendingDeliveries . get ( msg . deliveryId )
@@ -513,6 +583,25 @@ export class ClusterManager {
513583 case "pong" : {
514584 break
515585 }
586+ case "reset" : {
587+ const resetMsg = msg as WsResetMessage
588+ const ws = this . workerConnections . get ( workerAddress )
589+ if ( ! ws ) break
590+ try {
591+ if ( ! this . resetHandler ) {
592+ throw new Error ( "No reset handler set on leader" )
593+ }
594+ await this . resetHandler ( resetMsg . position )
595+ ws . send ( JSON . stringify ( { type : "reset-ack" , resetId : resetMsg . resetId } ) )
596+ } catch ( err ) {
597+ ws . send ( JSON . stringify ( {
598+ type : "reset-fail" ,
599+ resetId : resetMsg . resetId ,
600+ error : err instanceof Error ? err . message : String ( err ) ,
601+ } ) )
602+ }
603+ break
604+ }
516605 default :
517606 break
518607 }
@@ -582,5 +671,11 @@ export class ClusterManager {
582671 delivery . reject ( new Error ( `Delivery ${ id } to ${ delivery . workerAddress } timed out` ) )
583672 }
584673 }
674+ for ( const [ id , pending ] of this . pendingResets ) {
675+ if ( now - pending . sentAt > this . deliveryTimeoutMs ) {
676+ this . pendingResets . delete ( id )
677+ pending . reject ( new Error ( `Reset ${ id } timed out` ) )
678+ }
679+ }
585680 }
586681}
0 commit comments