updateLike in src/services/likeService.ts reads the entire like` JSON column, mutates it in memory, and then writes it back without any locking or atomicity. Under concurrency this leads to lost likes.
Example race:
Request A reads: { likes: [user1] }
Request B reads: { likes: [user1] }
Request A writes: { likes: [user1, user2] }
Request B writes: { likes: [user1, user3] } → user2’s like is lost
Evidence:
-
Line 33: result?.like — full JSON read from DB
-
Line 41: newLikes = resLikes.filter(...) — in‑memory mutation
-
Lines 65–72: prisma.submission.update({ data: { like: newLikes } }) — blind write with no version/version check
Root cause: the like JSON column is used as a mutable array with no optimistic locking or atomic update, and like + likeCount are updated independently. Prisma does not support compare‑and‑swap on JSON fields, so concurrent updates overwrite each other.
Proposed fix:
- Use SQL
transactions and FOR UPDATE syntax for atomic updates instead of introducing a new table
- Use Promise.all for real time concurrency test.
- other recommendations for further classification, changing “toggle” into explicit PUT like / DELETE like operations later, since toggles can behave unexpectedly when clients retry requests.
Reference PR: #1411
updateLike
in src/services/likeService.tsreads the entire like` JSON column, mutates it in memory, and then writes it back without any locking or atomicity. Under concurrency this leads to lost likes.Example race:
Request A reads:
{ likes: [user1] }Request B reads:
{ likes: [user1] }Request A writes:
{ likes: [user1, user2] }Request B writes:
{ likes: [user1, user3] }→ user2’s like is lostEvidence:
Line 33:
result?.like— full JSON read from DBLine 41:
newLikes = resLikes.filter(...)— in‑memory mutationLines 65–72:
prisma.submission.update({ data: { like: newLikes } })— blind write with no version/version checkRoot cause: the
likeJSON column is used as a mutable array with no optimistic locking or atomic update, and like+ likeCountare updated independently. Prisma does not support compare‑and‑swap on JSON fields, so concurrent updates overwrite each other.Proposed fix:
transactionsandFOR UPDATEsyntax for atomic updates instead of introducing a new tableReference PR: #1411