Skip to content

Commit 3d339ae

Browse files
committed
fix(lobby): ensure atomic lobby and canvas creation with cleanup
1 parent b861e99 commit 3d339ae

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

server/src/models/Lobby.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ lobbySchema.statics.createWithCanvas = async function (name: string, ownerId?: s
5151
const lobbyId = new Types.ObjectId();
5252
const canvasId = new Types.ObjectId();
5353

54-
// Initialized to 0 (Transparent/White depending on palette)
5554
const emptyBuffer = Buffer.alloc(width * height, 0);
5655

5756
const canvas = new Canvas({
@@ -72,9 +71,15 @@ lobbySchema.statics.createWithCanvas = async function (name: string, ownerId?: s
7271
canvas: canvasId
7372
});
7473

75-
// Save both
76-
await Promise.all([canvas.save(), lobby.save()]);
77-
return lobby;
74+
try {
75+
await canvas.save();
76+
await lobby.save();
77+
return lobby;
78+
} catch (error) {
79+
// Cleanup canvas if lobby save fails
80+
await Canvas.findByIdAndDelete(canvasId);
81+
throw error;
82+
}
7883
};
7984

8085
export const Lobby = model<ILobby, LobbyModel>('Lobby', lobbySchema);

server/test/lobby.creation.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { Lobby } from '../src/models/Lobby.js';
3+
import { Canvas } from '../src/models/Canvas.js';
4+
5+
describe('Lobby Model - createWithCanvas Atomicity', () => {
6+
beforeEach(() => {
7+
vi.clearAllMocks();
8+
});
9+
10+
it('should not leave an orphaned Canvas if Lobby save fails', async () => {
11+
// 1. Mock Canvas.prototype.save to succeed
12+
const canvasSaveSpy = vi.spyOn(Canvas.prototype, 'save').mockResolvedValue({} as any);
13+
14+
// 2. Mock Lobby.prototype.save to fail (e.g., duplicate name)
15+
const lobbySaveSpy = vi.spyOn(Lobby.prototype, 'save').mockRejectedValue(new Error('Duplicate name'));
16+
17+
// 3. Mock Canvas.findByIdAndDelete for cleanup verification
18+
const canvasDeleteSpy = vi.spyOn(Canvas, 'findByIdAndDelete').mockResolvedValue({} as any);
19+
20+
// Attempt creation
21+
try {
22+
await Lobby.createWithCanvas('Failing Lobby');
23+
} catch (error: any) {
24+
expect(error.message).toBe('Duplicate name');
25+
}
26+
27+
// Verify it attempted to save both
28+
expect(canvasSaveSpy).toHaveBeenCalled();
29+
expect(lobbySaveSpy).toHaveBeenCalled();
30+
31+
// MANDATORY: Check if cleanup was triggered
32+
expect(canvasDeleteSpy).toHaveBeenCalled();
33+
});
34+
});

0 commit comments

Comments
 (0)