fix(offline): delete a finished local game from the device, not the network
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
The lobby's finished-game delete (hide) always called gateway.hideGame; for a local (offline) game the transport kill switch refused it, so it toasted and the game stayed. Route by id: a local game is removed from the device store + the source cache (LocalSource.delete), an online game still hides on the backend.
This commit is contained in:
@@ -40,6 +40,7 @@ export const localSource = {
|
||||
draftSave: (_id, _json) => load().then((s) => s.draftSave()),
|
||||
create: (opts) => load().then((s) => s.create(opts)),
|
||||
list: () => load().then((s) => s.list()),
|
||||
delete: (id) => load().then((s) => s.delete(id)),
|
||||
// events must return the unsubscribe synchronously (the screen subscribes in onMount), so it wires
|
||||
// the real subscription once the engine loads and, until then, cancels a pending subscribe.
|
||||
events: (id, onEvent) => {
|
||||
@@ -53,7 +54,7 @@ export const localSource = {
|
||||
unsub();
|
||||
};
|
||||
},
|
||||
} satisfies GameLoopSource & Pick<LocalSource, 'events' | 'create' | 'list'>;
|
||||
} satisfies GameLoopSource & Pick<LocalSource, 'events' | 'create' | 'list' | 'delete'>;
|
||||
|
||||
/**
|
||||
* gameSource returns the source that runs the game with the given id: the local engine for a local
|
||||
|
||||
@@ -65,6 +65,14 @@ describe('LocalSource.list', () => {
|
||||
expect(g.seats[1].accountId).not.toBe('acct-42'); // the robot keeps a synthetic id
|
||||
});
|
||||
|
||||
it('delete removes a stored local game from the list', async () => {
|
||||
const src = new LocalSource();
|
||||
await src.create(opts('local:x', 5n));
|
||||
expect((await src.list()).map((g) => g.id)).toContain('local:x');
|
||||
await src.delete('local:x');
|
||||
expect((await src.list()).map((g) => g.id)).not.toContain('local:x');
|
||||
});
|
||||
|
||||
it('reconstructs a game persisted by a previous session (not in the source cache)', async () => {
|
||||
// Seed the store via one source, then list from a fresh source whose in-memory cache is empty.
|
||||
await new LocalSource().create(opts('local:old', 7n));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { LocalGame, type LocalMove } from './engine';
|
||||
import { serializeGame, replayGame, type LocalGameRecord, type Seat as LocalSeat } from './serialize';
|
||||
import { getLocalGame, saveLocalGame, listLocalGames } from './store';
|
||||
import { getLocalGame, saveLocalGame, listLocalGames, deleteLocalGame } from './store';
|
||||
import { RULESETS } from './ruleset';
|
||||
import { getDawg } from '../dict';
|
||||
import { LOCAL_ID_PREFIX, isLocalGameId } from './id';
|
||||
@@ -151,6 +151,14 @@ export class LocalSource implements GameLoopSource {
|
||||
return views;
|
||||
}
|
||||
|
||||
/** delete removes a local game from the store and the in-memory cache — the offline lobby's
|
||||
* finished-game delete. Best-effort: a failed store delete still drops it from this session. */
|
||||
async delete(gameId: string): Promise<void> {
|
||||
this.live.delete(gameId);
|
||||
this.listeners.delete(gameId);
|
||||
await deleteLocalGame(gameId);
|
||||
}
|
||||
|
||||
async gameState(gameId: string): Promise<StateView> {
|
||||
return this.stateView(await this.load(gameId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user