@@ -169,120 +169,197 @@ procedure QueuePythiaWarning(const Pythia: IPythiaBrowser; const Message: string
169169 end );
170170end ;
171171
172- { TAnthropicClientUtils }
172+ { TFileRetrievalContext }
173+
174+ { --- Shared state for a WhenAllRetrieve run, lifted out of the promise
175+ executor's activation record to break the ARC cycle that arose
176+ when the prior implementation stored SettleResolve / SettleReject /
177+ StartOne as local closure variables inside that ActRec:
178+
179+ AR.SettleResolve -> AR_SettleResolve_body -> AR
180+
181+ The body of each helper captured the outer AR (to access Names,
182+ Settled, Resolve, Reject), while AR itself kept the helper's
183+ interface as a local field. Once the inner Files.Retrieve promises
184+ resolved and cleared their handler lists, no external reference
185+ remained, but the in-AR cycle kept the whole graph alive forever.
186+
187+ Holding the state in a TInterfacedObject and having the inner
188+ closures capture only an IInterface-typed Self breaks the cycle:
189+ when the inner promises drop their handlers, the interface
190+ refcount falls to zero and the context is destroyed in the
191+ normal ARC path. }
192+ type
193+ IFileRetrievalContext = interface
194+ [' {9E2F4D71-1C68-4A8E-9F3C-1A2B3C4D5E60}' ]
195+ function IsSettled : Boolean;
196+ procedure CompleteOne (Idx: Integer; const Filename: string);
197+ procedure SettleReject (const Msg: string);
198+ procedure DispatchAll ;
199+ end ;
173200
174- function TAnthropicClientUtils.WhenAllRetrieve (
175- const IDs: TArray<string>): TPromise<TArray<string>>;
201+ TFileRetrievalContext = class (TInterfacedObject, IFileRetrievalContext)
202+ private
203+ FClient: IAnthropic;
204+ FIDs: TArray<string>;
205+ FNames: TArray<string>;
206+ FRemaining: Integer;
207+ FSettled: Boolean;
208+ FResolve: TProc<TArray<string>>;
209+ FReject: TProc<Exception>;
210+ procedure DispatchOne (Idx: Integer);
211+ procedure SettleResolve ;
212+ public
213+ constructor Create(
214+ const AClient: IAnthropic;
215+ const AIDs: TArray<string>;
216+ const AResolve: TProc<TArray<string>>;
217+ const AReject: TProc<Exception>);
218+ function IsSettled : Boolean;
219+ procedure CompleteOne (Idx: Integer; const Filename: string);
220+ procedure SettleReject (const Msg: string);
221+ procedure DispatchAll ;
222+ end ;
223+
224+ constructor TFileRetrievalContext.Create(
225+ const AClient: IAnthropic;
226+ const AIDs: TArray<string>;
227+ const AResolve: TProc<TArray<string>>;
228+ const AReject: TProc<Exception>);
176229begin
177- Result := TPromise<TArray<string>>.Create(
178- procedure (Resolve: TProc<TArray<string>>; Reject: TProc<Exception>)
179- var
180- Names: TArray<string>;
181- Remaining: Integer;
182- Settled: Boolean;
183- begin
184- SetLength(Names, Length(IDs));
185- Remaining := Length(IDs);
186- Settled := False;
230+ inherited Create;
231+ FClient := AClient;
232+ FIDs := AIDs;
233+ SetLength(FNames, Length(AIDs));
234+ FRemaining := Length(AIDs);
235+ FSettled := False;
236+ FResolve := AResolve;
237+ FReject := AReject;
238+ end ;
187239
188- if Remaining = 0 then
189- begin
190- Resolve(Names);
191- Exit;
192- end ;
240+ function TFileRetrievalContext.IsSettled : Boolean;
241+ begin
242+ Result := FSettled;
243+ end ;
193244
194- { --- Single-point settle helpers. All paths (sync setup failure, async
195- Then/Catch bodies) must funnel through these so the outer promise
196- is guaranteed to settle exactly once even when something throws
197- outside the framework's try/except (e.g. CloneException failure on
198- an exotic exception class, or a throw in a queued &Catch lambda). }
199- var SettleResolve: TProc :=
200- procedure
201- begin
202- if Settled then
203- Exit;
245+ procedure TFileRetrievalContext.SettleResolve ;
246+ begin
247+ if FSettled then
248+ Exit;
204249
205- Settled := True;
206- Resolve(Names);
207- end ;
250+ FSettled := True;
251+ if Assigned(FResolve) then
252+ FResolve(FNames);
253+ end ;
208254
209- var SettleReject: TProc<string> :=
210- procedure (Msg: string)
211- begin
212- if Settled then
213- Exit;
255+ procedure TFileRetrievalContext.SettleReject (const Msg: string);
256+ begin
257+ if FSettled then
258+ Exit;
214259
215- Settled := True;
216- try
217- Reject(Exception.Create(Msg));
218- except
219- { --- Last-resort guard: never let an exception escape the queued
220- lambda, otherwise the outer promise stays pending forever and
221- the surrounding flow never emits TFinalizeData. }
222- end ;
223- end ;
260+ FSettled := True;
261+ try
262+ if Assigned(FReject) then
263+ FReject(Exception.Create(Msg));
264+ except
265+ { --- Last-resort guard: never let an exception escape the queued
266+ lambda, otherwise the outer promise stays pending forever and
267+ the surrounding flow never emits TFinalizeData. }
268+ end ;
269+ end ;
270+
271+ procedure TFileRetrievalContext.CompleteOne (
272+ Idx: Integer;
273+ const Filename: string);
274+ begin
275+ if FSettled then
276+ Exit;
224277
225- { --- Per-iteration capture: an inline var Idx := I inside the for body
226- does NOT create a fresh slot per iteration in Delphi (the begin..end
227- block is shared by all iterations), so all inner closures would
228- capture the same Idx and only the last index would be written.
229- Wrapping the body in an anonymous method called with I as a
230- parameter forces a new stack frame per call, giving each closure
231- its own captured Idx. }
232- var StartOne: TProc<Integer> :=
233- procedure (Idx: Integer)
278+ FNames[Idx] := Filename;
279+ Dec(FRemaining);
280+
281+ if FRemaining = 0 then
282+ SettleResolve;
283+ end ;
284+
285+ procedure TFileRetrievalContext.DispatchOne (Idx: Integer);
286+ var
287+ Ctx: IFileRetrievalContext;
288+ LocalId: string;
289+ begin
290+ { --- Capture Self as an explicit IInterface so the inner async
291+ closures keep the context alive via refcount; do NOT let the
292+ compiler capture the bare Self pointer, which would not pin
293+ the object's lifetime and could leave a dangling reference. }
294+ Ctx := Self;
295+ LocalId := FIDs[Idx];
296+
297+ try
298+ FClient.Files.AsyncAwaitRetrieve(LocalId)
299+ .&Then (
300+ procedure (Value : TFile)
234301 begin
302+ if Ctx.IsSettled then
303+ Exit;
304+
235305 try
236- FClient.Files.AsyncAwaitRetrieve(IDs[Idx])
237- .&Then (
238- procedure (Value : TFile)
239- begin
240- if Settled then
241- Exit;
242-
243- try
244- Names[Idx] := Value .Filename;
245- Dec(Remaining);
246-
247- if Remaining = 0 then
248- SettleResolve();
249- except
250- on E: Exception do
251- SettleReject(Format(' Files.Retrieve Then handler failed: %s (%s)' ,
252- [E.Message, E.ClassName]));
253- end ;
254- end )
255- .&Catch(
256- procedure (E: Exception)
257- begin
258- { --- Capture message immediately; CloneException on E later
259- (inside the framework) may dereference a freed object
260- or fail on an exotic class. Stringifying here is safe. }
261- var Msg := Format(' Files.Retrieve [%s] failed: %s (%s)' ,
262- [IDs[Idx], E.Message, E.ClassName]);
263-
264- SettleReject(Msg);
265- end );
306+ Ctx.CompleteOne(Idx, Value .Filename);
266307 except
267308 on E: Exception do
268- SettleReject(Format(' Files.Retrieve [%s] sync setup failed: %s (%s)' ,
269- [IDs[Idx], E.Message, E.ClassName]));
309+ Ctx. SettleReject(Format(' Files.Retrieve Then handler failed: %s (%s)' ,
310+ [E.Message, E.ClassName]));
270311 end ;
271- end ;
312+ end )
313+ .&Catch(
314+ procedure (E: Exception)
315+ begin
316+ { --- Capture message immediately; CloneException on E later
317+ (inside the framework) may dereference a freed object
318+ or fail on an exotic class. Stringifying here is safe. }
319+ Ctx.SettleReject(Format(' Files.Retrieve [%s] failed: %s (%s)' ,
320+ [LocalId, E.Message, E.ClassName]));
321+ end );
322+ except
323+ on E: Exception do
324+ Ctx.SettleReject(Format(' Files.Retrieve [%s] sync setup failed: %s (%s)' ,
325+ [LocalId, E.Message, E.ClassName]));
326+ end ;
327+ end ;
272328
273- try
274- for var I := Low(IDs) to High(IDs) do
275- begin
276- if Settled then
277- Break;
329+ procedure TFileRetrievalContext.DispatchAll ;
330+ begin
331+ if FRemaining = 0 then
332+ begin
333+ SettleResolve;
334+ Exit;
335+ end ;
278336
279- StartOne(I);
280- end ;
281- except
282- on E: Exception do
283- SettleReject(Format(' WhenAllRetrieve loop failed: %s (%s)' ,
284- [E.Message, E.ClassName]));
337+ try
338+ for var I := Low(FIDs) to High(FIDs) do
339+ begin
340+ if FSettled then
341+ Break;
342+
343+ DispatchOne(I);
285344 end ;
345+ except
346+ on E: Exception do
347+ SettleReject(Format(' WhenAllRetrieve loop failed: %s (%s)' ,
348+ [E.Message, E.ClassName]));
349+ end ;
350+ end ;
351+
352+ { TAnthropicClientUtils }
353+
354+ function TAnthropicClientUtils.WhenAllRetrieve (
355+ const IDs: TArray<string>): TPromise<TArray<string>>;
356+ begin
357+ Result := TPromise<TArray<string>>.Create(
358+ procedure (Resolve: TProc<TArray<string>>; Reject: TProc<Exception>)
359+ begin
360+ var Ctx: IFileRetrievalContext :=
361+ TFileRetrievalContext.Create(FClient, IDs, Resolve, Reject);
362+ Ctx.DispatchAll;
286363 end );
287364end ;
288365
0 commit comments