This commit is contained in:
+154
-2
@@ -1,10 +1,30 @@
|
||||
import Dexie, { Table } from "dexie";
|
||||
import { GameSession, AppSettings, SavedPlayer } from "../types";
|
||||
import {
|
||||
GameSession,
|
||||
AppSettings,
|
||||
SavedPlayer,
|
||||
Location,
|
||||
Profile,
|
||||
SyncState,
|
||||
} from "../types";
|
||||
import { generateId } from "../utils/id";
|
||||
|
||||
// When the sync engine applies records pulled/acked from the server, it flips
|
||||
// this flag so the auto-dirty hooks don't re-mark those writes as dirty.
|
||||
export const remoteApply = { active: false };
|
||||
|
||||
// The sync engine registers a (debounced) callback here so any local change to
|
||||
// a synced table promptly schedules a push. Kept as a hook to avoid a circular
|
||||
// import between db.ts and syncEngine.ts.
|
||||
export const localChange = { notify: null as null | (() => void) };
|
||||
|
||||
export class BoardScoreDatabase extends Dexie {
|
||||
sessions!: Table<GameSession, string>;
|
||||
settings!: Table<AppSettings, number>;
|
||||
players!: Table<SavedPlayer, string>;
|
||||
locations!: Table<Location, string>;
|
||||
profiles!: Table<Profile, string>;
|
||||
syncState!: Table<SyncState, string>;
|
||||
|
||||
constructor() {
|
||||
super("BoardScoreDatabase");
|
||||
@@ -20,16 +40,148 @@ export class BoardScoreDatabase extends Dexie {
|
||||
settings: "id",
|
||||
players: "id, name, createdAt",
|
||||
});
|
||||
|
||||
// Version 3: Added locations table, updatedAt tracking for future sync
|
||||
this.version(3)
|
||||
.stores({
|
||||
sessions: "id, gameId, dateStart, status, locationId, updatedAt",
|
||||
settings: "id",
|
||||
players: "id, name, createdAt, updatedAt",
|
||||
locations: "id, name, createdAt, updatedAt",
|
||||
})
|
||||
.upgrade(async (tx) => {
|
||||
await tx
|
||||
.table("sessions")
|
||||
.toCollection()
|
||||
.modify((session) => {
|
||||
session.updatedAt = session.dateEnd ?? session.dateStart;
|
||||
});
|
||||
await tx
|
||||
.table("players")
|
||||
.toCollection()
|
||||
.modify((player) => {
|
||||
player.updatedAt = player.createdAt;
|
||||
});
|
||||
});
|
||||
|
||||
// Version 4: Added local profiles; scope existing data to a default profile
|
||||
this.version(4)
|
||||
.stores({
|
||||
sessions:
|
||||
"id, gameId, dateStart, status, locationId, updatedAt, profileId",
|
||||
settings: "id",
|
||||
players: "id, name, createdAt, updatedAt, profileId",
|
||||
locations: "id, name, createdAt, updatedAt, profileId",
|
||||
profiles: "id, name, createdAt, updatedAt",
|
||||
})
|
||||
.upgrade(async (tx) => {
|
||||
const now = Date.now();
|
||||
const defaultProfileId = generateId();
|
||||
|
||||
await tx.table("profiles").add({
|
||||
id: defaultProfileId,
|
||||
name: "Moi",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
await tx
|
||||
.table("sessions")
|
||||
.toCollection()
|
||||
.modify((session) => {
|
||||
session.profileId = defaultProfileId;
|
||||
});
|
||||
await tx
|
||||
.table("players")
|
||||
.toCollection()
|
||||
.modify((player) => {
|
||||
player.profileId = defaultProfileId;
|
||||
});
|
||||
await tx
|
||||
.table("locations")
|
||||
.toCollection()
|
||||
.modify((location) => {
|
||||
location.profileId = defaultProfileId;
|
||||
});
|
||||
|
||||
const settings = await tx.table("settings").get(1);
|
||||
if (settings) {
|
||||
await tx
|
||||
.table("settings")
|
||||
.update(1, { activeProfileId: defaultProfileId });
|
||||
} else {
|
||||
await tx.table("settings").add({
|
||||
id: 1,
|
||||
theme: "system",
|
||||
language: "fr",
|
||||
activeProfileId: defaultProfileId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Version 5: Sync bookkeeping (dirty flag + sync cursor per profile)
|
||||
this.version(5)
|
||||
.stores({
|
||||
sessions:
|
||||
"id, gameId, dateStart, status, locationId, updatedAt, profileId, dirty",
|
||||
settings: "id",
|
||||
players: "id, name, createdAt, updatedAt, profileId, dirty",
|
||||
locations: "id, name, createdAt, updatedAt, profileId, dirty",
|
||||
profiles: "id, name, createdAt, updatedAt",
|
||||
syncState: "profileId",
|
||||
})
|
||||
.upgrade(async (tx) => {
|
||||
// Mark all pre-existing records dirty so they push on the first sync.
|
||||
for (const table of ["sessions", "players", "locations"]) {
|
||||
await tx
|
||||
.table(table)
|
||||
.toCollection()
|
||||
.modify((row) => {
|
||||
row.dirty = 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new BoardScoreDatabase();
|
||||
|
||||
// Initialize default settings if empty
|
||||
// ---- Auto-stamp updatedAt + dirty on every local write to synced tables ----
|
||||
// Skipped while the sync engine is applying server data (remoteApply.active).
|
||||
for (const table of [db.sessions, db.players, db.locations]) {
|
||||
table.hook("creating", (_primKey, obj: any) => {
|
||||
if (remoteApply.active) return;
|
||||
if (obj.updatedAt === undefined) obj.updatedAt = Date.now();
|
||||
if (obj.dirty === undefined) obj.dirty = 1;
|
||||
localChange.notify?.();
|
||||
});
|
||||
table.hook("updating", (modifications: any) => {
|
||||
if (remoteApply.active) return;
|
||||
// Don't clobber an explicit dirty/updatedAt already in this update.
|
||||
const extra: Record<string, unknown> = {};
|
||||
if (modifications.updatedAt === undefined) extra.updatedAt = Date.now();
|
||||
if (modifications.dirty === undefined) extra.dirty = 1;
|
||||
localChange.notify?.();
|
||||
return extra;
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize default profile + settings on a fresh install
|
||||
db.on("populate", async () => {
|
||||
const now = Date.now();
|
||||
const defaultProfileId = generateId();
|
||||
|
||||
await db.profiles.add({
|
||||
id: defaultProfileId,
|
||||
name: "Moi",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
await db.settings.add({
|
||||
id: 1,
|
||||
theme: "system",
|
||||
language: "fr",
|
||||
activeProfileId: defaultProfileId,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user