1+ function Try (Function , ...)
2+
3+ -- Capture function execution response
4+ local Data = { pcall (Function , ... ) };
5+
6+ -- Determine whether execution succeeded or failed
7+ local Success = Data [1 ];
8+
9+ -- Gather arguments to return from data
10+ local Arguments = { unpack (Data , 2 ) };
11+
12+ -- Return attempt for chaining
13+ return setmetatable ({
14+ _IsAttempt = true ,
15+ Then = Then ,
16+ Catch = Catch ,
17+ Retry = Retry ,
18+ Success = Success ,
19+ Arguments = Arguments ,
20+ Stack = debug.traceback (),
21+ LastArguments = { ... },
22+ Hops = (not Success ) and { Function } or nil ,
23+ RetryCount = 0 ,
24+ Start = true
25+
26+ -- Indicate type when converted to string (to aid in debugging)
27+ }, { __tostring = function () return ' Attempt' end })
28+
29+ end ;
30+
31+ function Then (Attempt , Callback )
32+
33+ -- Update attempt state
34+ Attempt .Start = false ;
35+
36+ -- Enter new attempt contexts if received
37+ local FirstArgument = Attempt .Arguments [1 ];
38+ if Attempt .Success and type (FirstArgument ) == ' table' and FirstArgument ._IsAttempt then
39+ Attempt = FirstArgument ;
40+ end ;
41+
42+ -- Skip processing if attempt failed
43+ if not Attempt .Success then
44+ table.insert (Attempt .Hops , Callback );
45+ return Attempt ;
46+ end ;
47+
48+ -- Capture callback execution response
49+ local Data = { pcall (Callback , unpack (Attempt .Arguments )) };
50+ local Success = Data [1 ];
51+ local Arguments = { unpack (Data , 2 ) };
52+
53+ -- Replace attempt state
54+ Attempt .Success = Success ;
55+ Attempt .LastArguments = Attempt .Arguments ;
56+ Attempt .Arguments = Arguments ;
57+ Attempt .Stack = debug.traceback ();
58+
59+ -- Track hops on failure
60+ if not Success then
61+ Attempt .Hops = { Callback };
62+ end
63+
64+ -- Return attempt for chaining
65+ return Attempt ;
66+
67+ end ;
68+
69+ function Catch (Attempt , ...)
70+
71+ -- Capture all arguments
72+ local Arguments = { ... };
73+
74+ -- Get target errors and the callback
75+ local TargetErrors = { unpack (Arguments , 1 , # Arguments - 1 ) };
76+ local Callback = unpack (Arguments , # Arguments );
77+
78+ -- Enter new attempt contexts if received
79+ local FirstArgument = Attempt .Arguments [1 ];
80+ if type (FirstArgument ) == ' table' and FirstArgument ._IsAttempt then
81+ Attempt = FirstArgument ;
82+ end ;
83+
84+ -- Proceed upon unhandled failure
85+ if not Attempt .Success and not Attempt .Handled then
86+
87+ -- Track hops
88+ table.insert (Attempt .Hops , Arguments );
89+
90+ -- Get error from failed attempt
91+ local Error = Attempt .Arguments [1 ];
92+
93+ -- Filter errors if target errors were specified
94+ if (# TargetErrors > 0 ) then
95+ for _ , TargetError in pairs (TargetErrors ) do
96+ if type (Error ) == ' string' and Error :match (TargetError ) then
97+ Attempt .Handled = true ;
98+ return Try (Callback , Error , Attempt .Stack , Attempt );
99+ end ;
100+ end ;
101+
102+ -- Pass any error if no target errors were specified
103+ elseif # TargetErrors == 0 then
104+ Attempt .Handled = true ;
105+ return Try (Callback , Error , Attempt .Stack , Attempt );
106+ end ;
107+
108+ end ;
109+
110+ -- Return attempt for chaining
111+ return Attempt ;
112+
113+ end ;
114+
115+ function Retry (Attempt )
116+
117+ -- Ensure attempt failed
118+ if Attempt .Success then
119+ return ;
120+ end ;
121+
122+ -- Get hops and arguments
123+ local Hops = Attempt .Hops ;
124+ local Arguments = Attempt .LastArguments ;
125+
126+ -- Reset attempt state
127+ Attempt .Hops = nil ;
128+ Attempt .Success = true ;
129+ Attempt .Arguments = Arguments ;
130+ Attempt .Handled = nil ;
131+ Attempt .RetryCount = Attempt .RetryCount and (Attempt .RetryCount + 1 ) or 1 ;
132+
133+ -- Restart attempts that failed from the start
134+ if Attempt .Start then
135+ local NewAttempt = Try (Hops [1 ], Arguments );
136+
137+ -- Reset retry counter if reattempt succeeds
138+ if NewAttempt .Success then
139+ NewAttempt .RetryCount = 0 ;
140+ else
141+ NewAttempt .RetryCount = Attempt .RetryCount ;
142+ end ;
143+
144+ -- Apply each hop
145+ for HopIndex , Hop in ipairs (Hops ) do
146+ if HopIndex > 1 then
147+
148+ -- Apply `then` hops
149+ if type (Hop ) == ' function' then
150+ NewAttempt :Then (Hop );
151+
152+ -- Apply `catch` hops
153+ elseif type (Hop ) == ' table' then
154+ NewAttempt :Catch (unpack (Hop ));
155+ end ;
156+
157+ end ;
158+ end ;
159+
160+ -- Return the new attempt
161+ return NewAttempt ;
162+
163+ -- Continue attempts that failed after the start
164+ else
165+ for HopIndex , Hop in ipairs (Hops ) do
166+
167+ -- Apply `then` hoops
168+ if type (Hop ) == ' function' then
169+ Attempt :Then (Hop );
170+
171+ -- Apply `catch` hops
172+ elseif type (Hop ) == ' table' then
173+ Attempt :Catch (unpack (Hop ));
174+ end ;
175+
176+ -- Reset retry counter if reattempt succeeds
177+ if HopIndex == 1 and Attempt .Success then
178+ Attempt .RetryCount = 0 ;
179+ end ;
180+
181+ end ;
182+
183+ -- Return the attempt
184+ return Attempt ;
185+ end ;
186+
187+ end ;
188+
189+ return Try ;
0 commit comments