source-cooperative / source-cooperative/source.coop
Product writes have no optimistic locking (lost-update risk)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 26
- Forks
- 9
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 42
Description
Summary
The product-mirror server actions — addProductMirror, removeProductMirror, setPrimaryMirror (src/lib/actions/product-mirrors.ts) — read a product, mutate metadata.mirrors / primary_mirror in memory, and write the whole product back via productsTable.update. There is no condition on the write, so two concurrent admin operations on the same product can silently clobber each other (lost update).
This was raised as a non-blocking finding in the #364 review.
Scope
This is not specific to mirrors: productsTable.update is a read-modify-write everywhere it's used, and Product has no version attribute. So a real fix is a small cross-cutting change, not a one-off — which is why it's split out of #364 rather than patched only in the mirror actions.
Impact
Low in practice: these are admin-only operations and concurrent edits of the same product are rare. But the race is real and silent when it happens.
Proposed fix
Add optimistic concurrency to productsTable.update:
- Condition the
UpdateCommandon theupdated_atthe caller read (ConditionExpression: "updated_at = :prev_updated_at"), or introduce a numericversionattribute and bump+condition it. - Surface
ConditionalCheckFailedExceptionto callers as a "this product changed, please retry" error.
Apply consistently across product mutations so the behavior is uniform.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/lib/actions/product-mirrors.ts and trace every use of productsTable.update, including addProductMirror, removeProductMirror, and setPrimaryMirror. Review how the caller-read updated_at or a version would be carried into the UpdateCommand and how ConditionalCheckFailedException is surfaced. Done means product mutations consistently reject stale writes with a retryable error instead of silently losing updates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100