Skip to content

Commit ce911cb

Browse files
committed
Demo VCL_Anthropic updated - SDK 1.3 supported
1 parent 2750acc commit ce911cb

7 files changed

Lines changed: 611 additions & 223 deletions

File tree

demos/VCL/pythia-anthropic/Demo.Anthropic.AsyncUtils.pas

Lines changed: 175 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -169,120 +169,197 @@ procedure QueuePythiaWarning(const Pythia: IPythiaBrowser; const Message: string
169169
end);
170170
end;
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>);
176229
begin
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);
287364
end;
288365

0 commit comments

Comments
 (0)