mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
fix(cycle): keep a feed turned off during a rotation off
Rotation revoked the feed it had read without checking it was still live, so a turn-off in between came back under the new link, and it copied options read before a concurrent change. The revoke is guarded and the options are read after it; PATCH writes only to the live feed and follows a rotation to its successor.
This commit is contained in:
1 parent
6fb0d184f1
commit
befbace445
4 files changed
+123
-13
No files matched your search
@@ -7,10 +7,11 @@
|
||||
* The forecast style is copied too: a feed minted before that option keeps
|
||||
* BAND rather than taking a new feed's default.
|
||||
* Calendars holding the old link stop updating on their next poll. The new
|
||||
* URL is returned exactly once. Two rotations racing both revoke the same
|
||||
* feed, and the second successor then meets the one-live-feed index: that
|
||||
* one gets the mint path's 409 `cycle.feed.exists`, and the winner's link
|
||||
* stands.
|
||||
* URL is returned exactly once. Of two rotations racing, the second finds
|
||||
* the feed already revoked (or its successor meets the one-live-feed index)
|
||||
* and gets the mint path's 409 `cycle.feed.exists`; the winner's link
|
||||
* stands. A subscription turned off during a rotation stays off, and an
|
||||
* option changed during one lands on the link that stays live.
|
||||
*
|
||||
* Same gates as turning the subscription on: cookie session only, the owner or
|
||||
* the guardian of a managed profile (`requireGuardianAuth`), a fresh proof,
|
||||
@@ -72,8 +73,23 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const minted = mintFeedToken();
|
||||
const rotated = await prisma
|
||||
.$transaction(async (tx) => {
|
||||
const live = await tx.cycleCalendarFeed.findFirst({
|
||||
const found = await tx.cycleCalendarFeed.findFirst({
|
||||
where: { userId: user.id, revokedAt: null },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!found) return null;
|
||||
// Only while it is still live: a subscription turned off after the
|
||||
// read above stays off instead of coming back under a new link.
|
||||
const revoked = await tx.cycleCalendarFeed.updateMany({
|
||||
where: { id: found.id, revokedAt: null },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
if (revoked.count === 0) return "raced" as const;
|
||||
// The options are read after the revoke holds the row, so a change
|
||||
// that committed in between is the one the new link carries, and one
|
||||
// that comes later waits and then finds the new link.
|
||||
const live = await tx.cycleCalendarFeed.findUniqueOrThrow({
|
||||
where: { id: found.id },
|
||||
select: {
|
||||
id: true,
|
||||
horizonCycles: true,
|
||||
@@ -83,11 +99,6 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
forecastStyle: true,
|
||||
},
|
||||
});
|
||||
if (!live) return null;
|
||||
await tx.cycleCalendarFeed.update({
|
||||
where: { id: live.id },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
const created = await tx.cycleCalendarFeed.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
|
||||
@@ -170,7 +170,7 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
);
|
||||
}
|
||||
|
||||
const live = await findLiveFeed(user.id);
|
||||
let live = await findLiveFeed(user.id);
|
||||
if (!live) return noLiveFeed();
|
||||
|
||||
const data = {
|
||||
@@ -188,9 +188,25 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
? { forecastStyle: input.forecastStyle }
|
||||
: {}),
|
||||
};
|
||||
const updated = await prisma.cycleCalendarFeed.update({
|
||||
where: { id: live.id },
|
||||
// Only while the feed is still the live one. A rotation that committed
|
||||
// after the read above revoked it and copied its old options onto the new
|
||||
// link; the change then goes to that link, once. Updating the revoked row
|
||||
// would answer 200 with the old options left in force.
|
||||
let written = await prisma.cycleCalendarFeed.updateMany({
|
||||
where: { id: live.id, revokedAt: null },
|
||||
data,
|
||||
});
|
||||
if (written.count === 0) {
|
||||
live = await findLiveFeed(user.id);
|
||||
if (!live) return noLiveFeed();
|
||||
written = await prisma.cycleCalendarFeed.updateMany({
|
||||
where: { id: live.id, revokedAt: null },
|
||||
data,
|
||||
});
|
||||
if (written.count === 0) return feedExists();
|
||||
}
|
||||
const updated = await prisma.cycleCalendarFeed.findUniqueOrThrow({
|
||||
where: { id: live.id },
|
||||
select: FEED_SUMMARY_SELECT,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user