Skip to content

Commit 02c3991

Browse files
committed
Javascript Binding - IJavascriptCallbackProxy.ExecuteAsync correctly creates pending task using callback Id
- Previously a new PendingTask(TaskCompletionSource) was created using a new Id which worked when the Ids matched between the browser and render process. If you skipped executing a callback then the Ids would get out of sync. Issue #3979
1 parent b50c60e commit 02c3991

File tree

6 files changed

+190
-16
lines changed

6 files changed

+190
-16
lines changed

CefSharp.Core.Runtime/Internals/ClientAdapter.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,10 @@ namespace CefSharp
13931393
auto success = argList->GetBool(0);
13941394
auto callbackId = GetInt64(argList, 1);
13951395

1396-
auto pendingTask = _pendingTaskRepository->RemovePendingTask(callbackId);
1396+
auto pendingTask = name == kEvaluateJavascriptResponse ?
1397+
_pendingTaskRepository->RemovePendingTask(callbackId) :
1398+
_pendingTaskRepository->RemoveJavascriptCallbackPendingTask(callbackId);
1399+
13971400
if (pendingTask != nullptr)
13981401
{
13991402
auto response = gcnew JavascriptResponse();

CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ namespace CefSharp
3333
auto browserWrapper = static_cast<CefBrowserWrapper^>(browser);
3434
auto javascriptNameConverter = GetJavascriptNameConverter();
3535

36-
auto doneCallback = _pendingTasks->CreatePendingTask(timeout);
36+
auto doneCallback = _pendingTasks->CreateJavascriptCallbackPendingTask(_callback->Id, timeout);
3737

3838
auto callbackMessage = CefProcessMessage::Create(kJavascriptCallbackRequest);
3939
auto argList = callbackMessage->GetArgumentList();

CefSharp.Example/JavascriptBinding/AsyncBoundObject.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,5 +256,20 @@ public uint UIntAdd(uint paramA, uint paramB)
256256
{
257257
return paramA + paramB;
258258
}
259+
260+
public async Task<string> JavascriptOptionalCallbackEvalPromise(bool invokeCallback, string msg, IJavascriptCallback callback)
261+
{
262+
using (callback)
263+
{
264+
if (invokeCallback)
265+
{
266+
var response = await callback.ExecuteAsync(callback.Id, msg).ConfigureAwait(false);
267+
//Echo the response
268+
return (string)response.Result;
269+
}
270+
271+
return msg;
272+
}
273+
}
259274
}
260275
}

CefSharp.Example/Resources/BindingTestSingle.html

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,60 @@
1717
await CefSharp.BindObjectAsync("boundAsync");
1818
});
1919

20-
QUnit.test("Async call (Divide 16 / 2):", async (assert) =>
20+
QUnit.test("Pass callback(promise) that is conditionally invoked:", async (assert) =>
2121
{
22-
const expectedResult = 8
23-
const actualResult = await boundAsync.div(16, 2);
22+
let convertPromiseToCefSharpCallback = function (p)
23+
{
24+
let f = function (callbackId, ...args)
25+
{
26+
//We immediately return CefSharpDefEvalScriptRes as we will be
27+
//using a promise and will call sendEvalScriptResponse when our
28+
//promise has completed
29+
(async function ()
30+
{
31+
try
32+
{
33+
//Await the promise
34+
let response = await p(...args);
2435

25-
assert.equal(expectedResult, actualResult, "Divide 16 / 2 resulted in " + expectedResult);
36+
//We're done, let's send our response back to our .Net App
37+
cefSharp.sendEvalScriptResponse(callbackId, true, response, true);
38+
}
39+
catch (err)
40+
{
41+
//An error occurred let's send the response back to our .Net App
42+
cefSharp.sendEvalScriptResponse(callbackId, false, err.message, true);
43+
}
44+
})();
45+
46+
//Let CefSharp know we're going to be defering our response as we have some async/await
47+
//processing to happen before our callback returns it's value
48+
return "CefSharpDefEvalScriptRes";
49+
}
50+
51+
return f;
52+
}
53+
54+
//The function convertPromiseToCefSharpCallback can be used in your own projects
55+
//Pass in a function that wraps your promise, not your promise directly
56+
let callback = convertPromiseToCefSharpCallback(function (msg)
57+
{
58+
return new Promise((resolve, reject) =>
59+
{
60+
setTimeout(() =>
61+
{
62+
resolve(msg);
63+
}, 300);
64+
});
65+
});
66+
67+
const expectedResult = "Callback has not been invoked";
68+
const actualResult = await boundAsync.javascriptOptionalCallbackEvalPromise(false, expectedResult, callback);
69+
assert.equal(expectedResult, actualResult, "Echo response after promise execution");
70+
71+
const expectedSecondResult = "Callback has been invoked";
72+
const actualSecondResult = await boundAsync.javascriptOptionalCallbackEvalPromise(true, expectedSecondResult, callback);
73+
assert.equal(expectedSecondResult, actualSecondResult, "Echo response after promise execution");
2674
});
2775
});
2876
</script>

CefSharp.Example/Resources/BindingTestsAsyncTask.html

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,6 @@
9898

9999
QUnit.test("JavascriptCallback EvalPromise:", async (assert) =>
100100
{
101-
const expectedResult = "JavascriptCallback after promise";
102-
103101
let convertPromiseToCefSharpCallback = function (p)
104102
{
105103
let f = function (callbackId, ...args)
@@ -135,21 +133,79 @@
135133

136134
//The function convertPromiseToCefSharpCallback can be used in your own projects
137135
//Pass in a function that wraps your promise, not your promise directly
138-
let callback = convertPromiseToCefSharpCallback(function ()
136+
let callback = convertPromiseToCefSharpCallback(function (msg)
139137
{
140138
return new Promise((resolve, reject) =>
141139
{
142140
setTimeout(() =>
143141
{
144-
resolve(expectedResult);
142+
resolve(msg);
145143
}, 300);
146144
});
147145
});
148146

147+
const expectedResult = "JavascriptCallback after promise";
149148
const actualResult = await boundAsync.javascriptCallbackEvalPromise(expectedResult, callback);
150149

151150
assert.equal(expectedResult, actualResult, "Echo response after promise execution");
152151
});
152+
153+
// Issue #3979
154+
QUnit.test("JavascriptCallback conditionally EvalPromise:", async (assert) =>
155+
{
156+
let convertPromiseToCefSharpCallback = function (p)
157+
{
158+
let f = function (callbackId, ...args)
159+
{
160+
//We immediately return CefSharpDefEvalScriptRes as we will be
161+
//using a promise and will call sendEvalScriptResponse when our
162+
//promise has completed
163+
(async function ()
164+
{
165+
try
166+
{
167+
//Await the promise
168+
let response = await p(...args);
169+
170+
//We're done, let's send our response back to our .Net App
171+
cefSharp.sendEvalScriptResponse(callbackId, true, response, true);
172+
}
173+
catch (err)
174+
{
175+
//An error occurred let's send the response back to our .Net App
176+
cefSharp.sendEvalScriptResponse(callbackId, false, err.message, true);
177+
}
178+
})();
179+
180+
//Let CefSharp know we're going to be defering our response as we have some async/await
181+
//processing to happen before our callback returns it's value
182+
return "CefSharpDefEvalScriptRes";
183+
}
184+
185+
return f;
186+
}
187+
188+
//The function convertPromiseToCefSharpCallback can be used in your own projects
189+
//Pass in a function that wraps your promise, not your promise directly
190+
let callback = convertPromiseToCefSharpCallback(function (msg)
191+
{
192+
return new Promise((resolve, reject) =>
193+
{
194+
setTimeout(() =>
195+
{
196+
resolve(msg);
197+
}, 300);
198+
});
199+
});
200+
201+
const expectedResult = "Callback has not been invoked";
202+
const actualResult = await boundAsync.javascriptOptionalCallbackEvalPromise(false, expectedResult, callback);
203+
assert.equal(expectedResult, actualResult, "Echo response after promise execution");
204+
205+
const expectedSecondResult = "Callback has been invoked";
206+
const actualSecondResult = await boundAsync.javascriptOptionalCallbackEvalPromise(true, expectedSecondResult, callback);
207+
assert.equal(expectedSecondResult, actualSecondResult, "Echo response after promise execution");
208+
});
153209
});
154210
</script>
155211

CefSharp/Internals/PendingTaskRepository.cs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@
1111
namespace CefSharp.Internals
1212
{
1313
/// <summary>
14-
/// Class to store TaskCompletionSources indexed by a unique id.
14+
/// Class to store TaskCompletionSources indexed by a unique id. There are two distinct ConcurrentDictionary
15+
/// instances as we have some Tasks that are created from the browser process (EvaluateScriptAsync) calls, and
16+
/// some that are created for <see cref="IJavascriptCallback"/> instances for which the Id's are created
17+
/// in the render process.
1518
/// </summary>
1619
/// <typeparam name="TResult">The type of the result produced by the tasks held.</typeparam>
1720
public sealed class PendingTaskRepository<TResult>
1821
{
1922
private readonly ConcurrentDictionary<long, TaskCompletionSource<TResult>> pendingTasks =
2023
new ConcurrentDictionary<long, TaskCompletionSource<TResult>>();
24+
private readonly ConcurrentDictionary<long, TaskCompletionSource<TResult>> callbackPendingTasks =
25+
new ConcurrentDictionary<long, TaskCompletionSource<TResult>>();
2126
//should only be accessed by Interlocked.Increment
2227
private long lastId;
2328

@@ -42,17 +47,64 @@ public KeyValuePair<long, TaskCompletionSource<TResult>> CreatePendingTask(TimeS
4247
}
4348

4449
/// <summary>
45-
/// Gets and removed pending task by id.
50+
/// Creates a new pending task with a timeout.
51+
/// </summary>
52+
/// <param name="id">Id passed in from the render process</param>
53+
/// <param name="timeout">The maximum running time of the task.</param>
54+
/// <returns>The unique id of the newly created pending task and the newly created <see cref="TaskCompletionSource{TResult}"/>.</returns>
55+
public KeyValuePair<long, TaskCompletionSource<TResult>> CreateJavascriptCallbackPendingTask(long id, TimeSpan? timeout = null)
56+
{
57+
var taskCompletionSource = new TaskCompletionSource<TResult>();
58+
59+
callbackPendingTasks.TryAdd(id, taskCompletionSource);
60+
61+
if (timeout.HasValue)
62+
{
63+
taskCompletionSource = taskCompletionSource.WithTimeout(timeout.Value, () => RemoveJavascriptCallbackPendingTask(id));
64+
}
65+
66+
return new KeyValuePair<long, TaskCompletionSource<TResult>>(id, taskCompletionSource);
67+
}
68+
69+
/// <summary>
70+
/// If a <see cref="TaskCompletionSource{TResult}"/> is found matching <paramref name="id"/>
71+
/// then it is removed from the ConcurrentDictionary and returned.
4672
/// </summary>
4773
/// <param name="id">Unique id of the pending task.</param>
4874
/// <returns>
49-
/// The <see cref="TaskCompletionSource{TResult}"/> associated with the given id.
75+
/// The <see cref="TaskCompletionSource{TResult}"/> associated with the given id
76+
/// or null if no matching TaskComplectionSource found.
5077
/// </returns>
5178
public TaskCompletionSource<TResult> RemovePendingTask(long id)
5279
{
5380
TaskCompletionSource<TResult> result;
54-
pendingTasks.TryRemove(id, out result);
55-
return result;
81+
if (pendingTasks.TryRemove(id, out result))
82+
{
83+
return result;
84+
}
85+
86+
return null;
87+
}
88+
89+
/// <summary>
90+
/// If a <see cref="TaskCompletionSource{TResult}"/> is found matching <paramref name="id"/>
91+
/// then it is removed from the ConcurrentDictionary and returned.
92+
/// </summary>
93+
/// <param name="id">Unique id of the pending task.</param>
94+
/// <returns>
95+
/// The <see cref="TaskCompletionSource{TResult}"/> associated with the given id
96+
/// or null if no matching TaskComplectionSource found.
97+
/// </returns>
98+
public TaskCompletionSource<TResult> RemoveJavascriptCallbackPendingTask(long id)
99+
{
100+
TaskCompletionSource<TResult> result;
101+
102+
if (callbackPendingTasks.TryRemove(id, out result))
103+
{
104+
return result;
105+
}
106+
107+
return null;
56108
}
57109
}
58-
}
110+
}

0 commit comments

Comments
 (0)