I'm trying to figure out how Promises work with multiple HTTPS requests in Javascript, but I have a problem where the results seem out of order with the request sequence. I'm using an ASP.NET Controller API implementation for a simple calculator, and Javascript to access the API. I seem to have a synchronization issue, but I can't for the life of me work out why.
The CalculatorController
:
using Microsoft.AspNetCore.Mvc;
namespace Wolflight.Calculator.Controllers
{
[ApiController]
[Route("[controller]")]
public class CalculatorController : Controller
{
private const string TotalName = "Total";
private decimal RetrieveTotal()
{
return ToDecimal(HttpContext.Session.GetString(TotalName));
}
private void StoreTotal(decimal value)
{
HttpContext.Session.SetString(TotalName, FromDecimal(value));
}
private static string FromDecimal(decimal value)
{
return value.ToString();
}
private static decimal ToDecimal(string? value)
{
if (value != null)
{
return Decimal.Parse(value);
}
else
{
return 0M;
}
}
[HttpGet()]
[Route("/api/Calculator/Total")]
public decimal? GetTotal()
{
return RetrieveTotal();
}
[HttpPut()]
[Route("/api/Calculator/Add")]
public void AddValue(decimal value)
{
StoreTotal(RetrieveTotal() + value);
}
[HttpPut()]
[Route("/api/Calculator/Subtract")]
public void SubtractValue(decimal value)
{
StoreTotal(RetrieveTotal() - value);
}
[HttpPut()]
[Route("/api/Calculator/Multiply")]
public void MultiplyValue(decimal value)
{
StoreTotal(RetrieveTotal() * value);
}
[HttpPut()]
[Route("/api/Calculator/Divide")]
public void DivideValue(decimal value)
{
StoreTotal(RetrieveTotal() / value);
}
}
}
The site.js
:
const uriBase = "/api/Calculator/";
const uriTotal = uriBase + "Total";
const uriAdd = uriBase + "Add";
let GetTotalValuePromise = function () {
return new Promise(function (myResolve, myReject) {
let total = fetch(uriTotal)
.then(response => response.text())
.catch(error => myReject('Unable to get total.', error));
myResolve(total);
})
};
let PutAddValuePromise = function (addValue) {
return new Promise(function (myResolve, myReject) {
fetch(uriAdd + '?value=' + addValue, { method: 'PUT' })
.catch(error => myReject('Unable to add value.', error));
myResolve();
}
)
};
function DisplayTotal(total) {
const tBody = document.getElementById('totalDisplay');
tBody.innerHTML = total;
}
function GetTotal() {
UpdateDisplay();
}
function AddValue() {
let value = document.getElementById('addValue').value;
PutAddValuePromise(value)
.then(function () {
UpdateDisplay();
});
}
function UpdateDisplay() {
GetTotalValuePromise()
.then(
function (total) { DisplayTotal(total); },
function (message, error) { console.error(message, error); }
)
}
When I call AddValue()
from a form button, the result is that sometimes the /Total
call returns the value before the /Add
occurs, and sometimes it returns the result after.
e.g.
Total
= 0
AddValue
, with element addValue
as 5.Network Requests:
/Add?value=5
(no response)/Total
- Response: 0
.OR
Network Requests:
/Add?value=5
(no response)/Total
- Response: 5
.Am I missing something in how Promises work, or is the problem on the server side?
If I call GetTotal
manually after the AddValue
, it always returns the correct value.
You could try this below code. The issue you're facing this issue becaus new vlaue i snot being saved in the session and it is restyrning the old value before that.
To resolve this, add HttpContext.Session.CommitAsync().Wait() to ensure that the session state is persisted immediately after updating the total. use async/await in the clinetside code to execute the requests in the correct order, ensuring that the total is fetched only after the server has completed updating it.
Updated server-side code:
using Microsoft.AspNetCore.Mvc;
namespace CalculatorAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CalculatorController : Controller
{
private const string TotalName = "Total";
private decimal RetrieveTotal()
{
var value = decimal.TryParse(HttpContext.Session.GetString(TotalName), out var total) ? total : 0M;
Console.WriteLine($"RetrieveTotal: {value}");
return value;
}
private void StoreTotal(decimal value)
{
Console.WriteLine($"StoreTotal: {value}");
HttpContext.Session.SetString(TotalName, FromDecimal(value));
HttpContext.Session.CommitAsync().Wait(); // Ensure session state is persisted
}
private static string FromDecimal(decimal value)
{
return value.ToString();
}
[HttpGet("Total")]
public ActionResult<decimal> GetTotal()
{
return RetrieveTotal();
}
[HttpPut("Add")]
public IActionResult AddValue([FromQuery] decimal value)
{
StoreTotal(RetrieveTotal() + value);
return Ok();
}
[HttpPut("Subtract")]
public IActionResult SubtractValue([FromQuery] decimal value)
{
StoreTotal(RetrieveTotal() - value);
return Ok();
}
[HttpPut("Multiply")]
public IActionResult MultiplyValue([FromQuery] decimal value)
{
StoreTotal(RetrieveTotal() * value);
return Ok();
}
[HttpPut("Divide")]
public IActionResult DivideValue([FromQuery] decimal value)
{
if (value == 0)
return BadRequest("Cannot divide by zero.");
StoreTotal(RetrieveTotal() / value);
return Ok();
}
}
}
Client-side JavaScript:
const uriBase = "https://localhost:7262/api/Calculator/";
const uriTotal = uriBase + "Total";
const uriAdd = uriBase + "Add";
async function GetTotalValue() {
try {
const response = await fetch(uriTotal);
if (!response.ok) {
throw new Error('Failed to fetch total');
}
const total = await response.json(); // Parse the response as JSON
return total;
} catch (error) {
console.error("Error getting total:", error);
}
}
async function AddValue() {
const value = document.getElementById('addValue').value;
if (!value) {
alert("Please enter a value to add.");
return;
}
try {
const response = await fetch(`${uriAdd}?value=${value}`, { method: 'PUT' });
if (!response.ok) {
throw new Error('Failed to add value');
}
await UpdateDisplay(); // Refresh the display after updating the total
} catch (error) {
console.error("Error in AddValue:", error);
}
}
async function UpdateDisplay() {
const total = await GetTotalValue();
if (total !== undefined) {
document.getElementById('totalDisplay').innerText = total; // Update the total display
}
}
Program.cs:
builder.Services.AddDistributedMemoryCache(); // Required for session state
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30); // Set session timeout
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true; // Essential cookies for session
});