@@ -40,18 +40,31 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
4040 | Some s when s.NegotiatedFeatures.Contains flag -> Ok()
4141 | _ -> Error( ARCPError.InvalidRequest( sprintf " Feature %s was not negotiated" flag, None))
4242
43- let sendEnvelope ( env : Envelope ) : Task =
43+ let sendEnvelopeCt ( env : Envelope ) ( ct : CancellationToken ) : Task =
4444 let env =
4545 match sessionCtx with
4646 | Some s -> Envelope.withSessionId s.SessionId env
4747 | None -> env
4848
49- transport.SendAsync( env, receiveLoopCts.Token)
49+ transport.SendAsync( env, ct)
50+
51+ let sendEnvelope ( env : Envelope ) : Task = sendEnvelopeCt env receiveLoopCts.Token
5052
5153 let sendMessage ( msg : Message ) : Task =
5254 let env = Codec.toEnvelope msg
5355 sendEnvelope env
5456
57+ /// Await a correlated response while honoring the caller's token; on
58+ /// cancellation drop the pending entry so it does not leak (#98 ).
59+ let awaitResponse ( requestId : string ) ( waiter : Task < Envelope >) ( ct : CancellationToken ) : Task < Envelope > =
60+ task {
61+ try
62+ return ! waiter.WaitAsync( ct)
63+ with : ? OperationCanceledException as ex ->
64+ pending.Remove requestId
65+ return raise ex
66+ }
67+
5568 // Job-addressed envelopes can arrive before `SubmitAsync`/
5669 // `SubscribeAsync` register the handle (the receive loop completes
5770 // the request waiter and races ahead). Buffer such envelopes per
@@ -83,7 +96,10 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
8396 w.ResultSetter.TrySetResult( Ok payload) |> ignore
8497 | Message.JobError payload ->
8598 handles.TryRemove jid |> ignore
86- let err = JobErrorMapper.ofWire payload.Code payload.Message payload.Details jid
99+
100+ let err =
101+ JobErrorMapper.ofWireWith payload.Code payload.Message payload.Details payload.Retryable ( Some jid)
102+
87103 w.Channel.Writer.TryComplete() |> ignore
88104 w.ResultSetter.TrySetResult( Error err) |> ignore
89105 | _ -> ()
@@ -236,7 +252,7 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
236252 let env = Codec.toEnvelope ( Message.SessionHello( buildHello ()))
237253 let waiter = pending.Register env.Id
238254 do ! transport.SendAsync( env, ct)
239- let! welcomeEnv = waiter
255+ let! welcomeEnv = awaitResponse env.Id waiter ct
240256
241257 match Codec.toMessage welcomeEnv with
242258 | Ok( Message.SessionWelcome w) -> return acceptWelcome welcomeEnv w
@@ -269,8 +285,8 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
269285 | Ok() ->
270286 let env = Codec.toEnvelope ( Message.JobSubmit payload)
271287 let waiter = pending.Register env.Id
272- do ! sendEnvelope env
273- let! acceptedEnv = waiter
288+ do ! sendEnvelopeCt env ct
289+ let! acceptedEnv = awaitResponse env.Id waiter ct
274290
275291 match Codec.toMessage acceptedEnv with
276292 | Ok( Message.JobAccepted accepted) ->
@@ -293,8 +309,14 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
293309 registerHandle accepted.JobId writer
294310 return handle
295311 | Ok( Message.JobError errPayload) ->
312+ // §71: no job id context here — pass None.
296313 let err =
297- JobErrorMapper.ofWire errPayload.Code errPayload.Message errPayload.Details " "
314+ JobErrorMapper.ofWireWith
315+ errPayload.Code
316+ errPayload.Message
317+ errPayload.Details
318+ errPayload.Retryable
319+ None
298320
299321 return raise ( ArcpException err)
300322 | _ -> return raise ( ArcpException( ARCPError.InvalidRequest( " Expected job.accepted" , None)))
@@ -315,8 +337,8 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
315337
316338 let env = Codec.toEnvelope ( Message.JobSubscribe payload)
317339 let waiter = pending.Register env.Id
318- do ! sendEnvelope env
319- let! subscribedEnv = waiter
340+ do ! sendEnvelopeCt env ct
341+ let! subscribedEnv = awaitResponse env.Id waiter ct
320342
321343 // §7.6 / #96: surface subscription denials instead of
322344 // returning a live-looking handle.
@@ -329,7 +351,10 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
329351 registerHandle jobId.Value writer
330352 return handle
331353 | Ok( Message.SessionError e) ->
332- return raise ( ArcpException( JobErrorMapper.ofWire e.Code e.Message e.Details jobId.Value))
354+ return
355+ raise (
356+ ArcpException( JobErrorMapper.ofWireWith e.Code e.Message e.Details e.Retryable ( Some jobId.Value))
357+ )
333358 | _ -> return raise ( ArcpException( ARCPError.InvalidRequest( " Expected job.subscribed" , None)))
334359 }
335360
@@ -365,8 +390,8 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
365390
366391 let env = Codec.toEnvelope ( Message.SessionListJobs payload)
367392 let waiter = pending.Register env.Id
368- do ! sendEnvelope env
369- let! respEnv = waiter
393+ do ! sendEnvelopeCt env ct
394+ let! respEnv = awaitResponse env.Id waiter ct
370395
371396 match Codec.toMessage respEnv with
372397 | Ok( Message.SessionJobs jobs) -> return jobs
@@ -398,6 +423,11 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
398423 /// or fault). Lets callers observe that the client stopped pumping (#61 ).
399424 member _.Completion : Task = receiveLoopTask
400425
426+ /// Resolves with the negotiated session once ` session.welcome ` is
427+ /// received; faults if the handshake fails. A separate handle to the
428+ /// connect result for callers that did not await ` ConnectAsync ` (#70 ).
429+ member _.Connected : Task < SessionContext > = connectedTcs.Task
430+
401431 /// Close the session cleanly with an optional reason.
402432 member this.CloseAsync ( reason : string option , ct : CancellationToken ) : Task =
403433 task {
0 commit comments