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