fix: like button race condition
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^2.0.1",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"async-mutex": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.0.1",
|
||||
@@ -394,6 +395,14 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
|
||||
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-walk": {
|
||||
"version": "3.0.0-canary-5",
|
||||
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
||||
@@ -2882,6 +2891,11 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^2.0.1",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"async-mutex": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.0.1",
|
||||
|
@@ -5,6 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const slug = likeIcon.dataset.slug;
|
||||
const likeCountSpan = document.querySelector(`[data-like-count][data-slug="${slug}"]`);
|
||||
const storageKey = `liked-${slug}`;
|
||||
let isRequestInProgress = false;
|
||||
|
||||
// Function to update the UI based on liked state and count
|
||||
const updateUI = (isLiked, count) => {
|
||||
@@ -48,6 +49,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Add click event listener to the icon
|
||||
likeIcon.addEventListener('click', async () => {
|
||||
// If a request is already in progress, do nothing.
|
||||
if (isRequestInProgress) {
|
||||
return;
|
||||
}
|
||||
isRequestInProgress = true;
|
||||
|
||||
// Stop the hint animation if it's running
|
||||
likeIcon.classList.remove('hint-animation');
|
||||
// Toggle the liked state
|
||||
@@ -56,7 +63,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/likes/${slug}`, { method });
|
||||
if (!response.ok) return;
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const isNowLiked = !isCurrentlyLiked;
|
||||
@@ -67,6 +76,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error submitting like/unlike:', error);
|
||||
} finally {
|
||||
isRequestInProgress = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -3,10 +3,15 @@ const cors = require('cors');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { Mutex } = require('async-mutex');
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// Create mutexes to prevent race conditions when updating JSON files
|
||||
const viewsMutex = new Mutex();
|
||||
const likesMutex = new Mutex();
|
||||
|
||||
// trust proxy:
|
||||
// Used by express-rate-limit to obtain the client's IP address.
|
||||
// '1' means that the first hop (your reverse proxy) is trusted.
|
||||
@@ -97,6 +102,7 @@ app.get('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||
|
||||
// POST: Increment the view count for a specific post slug
|
||||
app.post('/api/views/:slug', validateSlug, async (req, res) => {
|
||||
const release = await viewsMutex.acquire();
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const views = await fs.readJson(dbPath);
|
||||
@@ -106,11 +112,14 @@ app.post('/api/views/:slug', validateSlug, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Error updating view count:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
|
||||
// POST: Increment the like count for a specific post slug
|
||||
app.post('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||
const release = await likesMutex.acquire();
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const likes = await fs.readJson(likesDbPath);
|
||||
@@ -120,11 +129,14 @@ app.post('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Error updating like count:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE: Decrement the like count for a specific post slug (unlike)
|
||||
app.delete('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||
const release = await likesMutex.acquire();
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const likes = await fs.readJson(likesDbPath);
|
||||
@@ -139,6 +151,8 @@ app.delete('/api/likes/:slug', validateSlug, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Error updating like count:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user