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