@@ -40,48 +40,70 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
4040 let env = Codec.toEnvelope msg
4141 sendEnvelope env
4242
43- let dispatchJobEvent ( env : Envelope ) ( payload : JobEventPayload ) : unit =
44- match env.JobId with
45- | None -> ()
46- | Some jid ->
47- match handles.TryGetValue jid with
48- | true , w ->
49- match payload.Body with
50- | JobEventBody.ResultChunk( rid, chunkSeq, data, enc, more) ->
51- let assembler = w.ChunkIndex.GetOrCreate rid
52-
53- match assembler.Append( chunkSeq, data, enc, more) with
54- | Ok _ -> w.Channel.Writer.TryWrite payload.Body |> ignore
55- | Error err ->
56- // Out-of-order or undecodable chunk: tear down
57- // the handle so callers don't sit on a job that
58- // will never produce a usable result.
59- handles.TryRemove jid |> ignore
60- w.Channel.Writer.TryComplete() |> ignore
61- w.ResultSetter.TrySetResult( Error err) |> ignore
62- | other -> w.Channel.Writer.TryWrite other |> ignore
63- | _ -> ()
64-
65- let dispatchJobResult ( env : Envelope ) ( payload : JobResultPayload ) : unit =
66- match env.JobId with
67- | None -> ()
68- | Some jid ->
69- match handles.TryRemove jid with
70- | true , w ->
71- w.Channel.Writer.TryComplete() |> ignore
72- w.ResultSetter.TrySetResult( Ok payload) |> ignore
73- | _ -> ()
43+ // Job-addressed envelopes can arrive before `SubmitAsync`/
44+ // `SubscribeAsync` register the handle (the receive loop completes
45+ // the request waiter and races ahead). Buffer such envelopes per
46+ // job id and flush them in order once the handle is registered, all
47+ // under one gate so registration and delivery cannot interleave (#95).
48+ let dispatchGate = obj ()
49+ let orphans = ConcurrentDictionary< string, ResizeArray< Envelope>>()
50+
51+ let deliver ( jid : string ) ( w : JobHandleWriter ) ( msg : Message ) : unit =
52+ match msg with
53+ | Message.JobEvent payload ->
54+ match payload.Body with
55+ | JobEventBody.ResultChunk( rid, chunkSeq, data, enc, more) ->
56+ let assembler = w.ChunkIndex.GetOrCreate rid
57+
58+ match assembler.Append( chunkSeq, data, enc, more) with
59+ | Ok _ -> w.Channel.Writer.TryWrite payload.Body |> ignore
60+ | Error err ->
61+ // Out-of-order or undecodable chunk: tear down the
62+ // handle so callers don't sit on a job that will never
63+ // produce a usable result.
64+ handles.TryRemove jid |> ignore
65+ w.Channel.Writer.TryComplete() |> ignore
66+ w.ResultSetter.TrySetResult( Error err) |> ignore
67+ | other -> w.Channel.Writer.TryWrite other |> ignore
68+ | Message.JobResult payload ->
69+ handles.TryRemove jid |> ignore
70+ w.Channel.Writer.TryComplete() |> ignore
71+ w.ResultSetter.TrySetResult( Ok payload) |> ignore
72+ | Message.JobError payload ->
73+ handles.TryRemove jid |> ignore
74+ let err = JobErrorMapper.ofWire payload.Code payload.Message payload.Details jid
75+ w.Channel.Writer.TryComplete() |> ignore
76+ w.ResultSetter.TrySetResult( Error err) |> ignore
77+ | _ -> ()
7478
75- let dispatchJobError ( env : Envelope ) ( payload : JobErrorPayload ) : unit =
79+ let dispatchJob ( env : Envelope ) ( msg : Message ) : unit =
7680 match env.JobId with
7781 | None -> ()
7882 | Some jid ->
79- match handles.TryRemove jid with
80- | true , w ->
81- let err = JobErrorMapper.ofWire payload.Code payload.Message payload.Details jid
82- w.Channel.Writer.TryComplete() |> ignore
83- w.ResultSetter.TrySetResult( Error err) |> ignore
84- | _ -> ()
83+ lock dispatchGate ( fun () ->
84+ match handles.TryGetValue jid with
85+ | true , w -> deliver jid w msg
86+ | _ ->
87+ // Buffer until the handle appears.
88+ let q = orphans.GetOrAdd( jid, ( fun _ -> ResizeArray< Envelope>()))
89+ q.Add env)
90+
91+ /// Register a job handle and flush any envelopes that arrived before
92+ /// it was known, preserving order (#95 ).
93+ let registerHandle ( jid : string ) ( w : JobHandleWriter ) : unit =
94+ lock dispatchGate ( fun () ->
95+ handles.[ jid] <- w
96+
97+ match orphans.TryRemove jid with
98+ | true , q ->
99+ for env in q do
100+ match Codec.toMessage env with
101+ | Ok m ->
102+ match handles.TryGetValue jid with
103+ | true , w2 -> deliver jid w2 m
104+ | _ -> ()
105+ | _ -> ()
106+ | _ -> ())
85107
86108 let onPing ( payload : SessionPingPayload ) : Task =
87109 let pong : SessionPongPayload =
@@ -128,14 +150,28 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
128150
129151 match msg with
130152 | Message.SessionPing p -> do ! onPing p
131- | Message.JobEvent p -> dispatchJobEvent env p
132- | Message.JobResult p -> dispatchJobResult env p
133- | Message.JobError p -> dispatchJobError env p
153+ | Message.JobEvent _
154+ | Message.JobResult _
155+ | Message.JobError _ -> dispatchJob env msg
134156 | _ -> ()
135157 with
136158 | : ? OperationCanceledException -> ()
137159 | ex -> pending.FailAll ex
138160 finally
161+ // §97: on any loop exit (clean EOF or cancellation) fault
162+ // every in-flight request waiter and complete every open
163+ // job handle so callers never hang forever.
164+ let closed = ARCPError.InternalError " ARCP transport closed"
165+ pending.FailAll( ArcpException closed)
166+
167+ lock dispatchGate ( fun () ->
168+ for kv in handles do
169+ kv.Value.Channel.Writer.TryComplete() |> ignore
170+ kv.Value.ResultSetter.TrySetResult( Error closed) |> ignore
171+
172+ handles.Clear()
173+ orphans.Clear())
174+
139175 ignore ( enumerator.DisposeAsync() .AsTask())
140176 }
141177 :> Task
@@ -237,7 +273,7 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
237273
238274 let credentials = accepted.Credentials |> Option.defaultValue []
239275 let handle , writer = mkHandle jid credentials cancelDelegate
240- handles .[ accepted.JobId] <- writer
276+ registerHandle accepted.JobId writer
241277 return handle
242278 | Ok( Message.JobError errPayload) ->
243279 let err =
@@ -263,14 +299,21 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
263299 let env = Codec.toEnvelope ( Message.JobSubscribe payload)
264300 let waiter = pending.Register env.Id
265301 do ! sendEnvelope env
266- let! _subscribed = waiter
302+ let! subscribedEnv = waiter
267303
268- let cancelDelegate ( _reason , _ct' ) =
269- task { return Error( ARCPError.PermissionDenied( " Subscribers cannot cancel" , None)) }
304+ // §7.6 / #96: surface subscription denials instead of
305+ // returning a live-looking handle.
306+ match Codec.toMessage subscribedEnv with
307+ | Ok( Message.JobSubscribed _) ->
308+ let cancelDelegate ( _reason , _ct' ) =
309+ task { return Error( ARCPError.PermissionDenied( " Subscribers cannot cancel" , None)) }
270310
271- let handle , writer = mkHandle jobId [] cancelDelegate
272- handles.[ jobId.Value] <- writer
273- return handle
311+ let handle , writer = mkHandle jobId [] cancelDelegate
312+ registerHandle jobId.Value writer
313+ return handle
314+ | Ok( Message.SessionError e) ->
315+ return raise ( ArcpException( JobErrorMapper.ofWire e.Code e.Message e.Details jobId.Value))
316+ | _ -> return raise ( ArcpException( ARCPError.InvalidRequest( " Expected job.subscribed" , None)))
274317 }
275318
276319 /// Stop receiving events for a subscribed job.
0 commit comments