refactor(skilltree): replace custom idb storage with @manacore/local-store

Remove the custom IndexedDB implementation (idb package + services/storage.ts)
and rewrite skills + achievements stores to use @manacore/local-store collections.

Changes:
- Rewrite skills.svelte.ts: all CRUD via skillCollection/activityCollection
- Rewrite achievements.svelte.ts: all persistence via achievementCollection
- Delete services/storage.ts (282 lines of custom idb code)
- Remove idb dependency from package.json
- Simplify layout comments

The stores now follow the same pattern as all other migrated apps:
reads/writes go to IndexedDB (Dexie.js), sync happens automatically
via mana-sync when authenticated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-27 21:32:47 +01:00
parent a31ccc6c62
commit 7754cf6e00
5 changed files with 218 additions and 472 deletions

View file

@ -49,7 +49,6 @@
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-ui": "workspace:^",
"@manacore/shared-utils": "workspace:*",
"idb": "^8.0.0",
"svelte-i18n": "^4.0.1",
"uuid": "^11.0.0"
},

View file

@ -1,281 +0,0 @@
import { openDB, type IDBPDatabase } from 'idb';
import type { Skill, Activity, UserStats, AchievementWithStatus } from '$lib/types';
interface SkillTreeDB {
skills: {
key: string;
value: Skill;
indexes: {
'by-branch': string;
'by-parent': string | null;
'by-level': number;
};
};
activities: {
key: string;
value: Activity;
indexes: {
'by-skill': string;
'by-timestamp': string;
};
};
stats: {
key: 'user-stats';
value: UserStats;
};
achievements: {
key: string;
value: AchievementWithStatus;
indexes: {
'by-category': string;
'by-unlocked': number;
};
};
}
const DB_NAME = 'skilltree-db';
const DB_VERSION = 2;
let dbPromise: Promise<IDBPDatabase<SkillTreeDB>> | null = null;
function getDB(): Promise<IDBPDatabase<SkillTreeDB>> {
if (!dbPromise) {
dbPromise = openDB<SkillTreeDB>(DB_NAME, DB_VERSION, {
upgrade(db) {
// Skills store
if (!db.objectStoreNames.contains('skills')) {
const skillStore = db.createObjectStore('skills', { keyPath: 'id' });
skillStore.createIndex('by-branch', 'branch');
skillStore.createIndex('by-parent', 'parentId');
skillStore.createIndex('by-level', 'level');
}
// Activities store
if (!db.objectStoreNames.contains('activities')) {
const activityStore = db.createObjectStore('activities', { keyPath: 'id' });
activityStore.createIndex('by-skill', 'skillId');
activityStore.createIndex('by-timestamp', 'timestamp');
}
// Stats store
if (!db.objectStoreNames.contains('stats')) {
db.createObjectStore('stats');
}
// Achievements store (added in v2)
if (!db.objectStoreNames.contains('achievements')) {
const achievementStore = db.createObjectStore('achievements', { keyPath: 'id' });
achievementStore.createIndex('by-category', 'category');
achievementStore.createIndex('by-unlocked', 'unlocked');
}
},
});
}
return dbPromise;
}
// Skills CRUD
export async function getAllSkills(): Promise<Skill[]> {
const db = await getDB();
return db.getAll('skills');
}
export async function getSkillById(id: string): Promise<Skill | undefined> {
const db = await getDB();
return db.get('skills', id);
}
export async function getSkillsByBranch(branch: string): Promise<Skill[]> {
const db = await getDB();
return db.getAllFromIndex('skills', 'by-branch', branch);
}
export async function getChildSkills(parentId: string): Promise<Skill[]> {
const db = await getDB();
return db.getAllFromIndex('skills', 'by-parent', parentId);
}
export async function saveSkill(skill: Skill): Promise<void> {
const db = await getDB();
skill.updatedAt = new Date().toISOString();
await db.put('skills', skill);
}
export async function deleteSkill(id: string): Promise<void> {
const db = await getDB();
// Delete skill and all its activities
const activities = await db.getAllFromIndex('activities', 'by-skill', id);
const tx = db.transaction(['skills', 'activities'], 'readwrite');
await Promise.all([
tx.objectStore('skills').delete(id),
...activities.map((a) => tx.objectStore('activities').delete(a.id)),
]);
await tx.done;
}
// Activities CRUD
export async function getAllActivities(): Promise<Activity[]> {
const db = await getDB();
return db.getAll('activities');
}
export async function getActivitiesBySkill(skillId: string): Promise<Activity[]> {
const db = await getDB();
return db.getAllFromIndex('activities', 'by-skill', skillId);
}
export async function getRecentActivities(limit = 10): Promise<Activity[]> {
const db = await getDB();
const all = await db.getAllFromIndex('activities', 'by-timestamp');
return all.reverse().slice(0, limit);
}
export async function saveActivity(activity: Activity): Promise<void> {
const db = await getDB();
await db.put('activities', activity);
}
export async function deleteActivity(id: string): Promise<void> {
const db = await getDB();
await db.delete('activities', id);
}
// User Stats
export async function getUserStats(): Promise<UserStats> {
const db = await getDB();
const stats = await db.get('stats', 'user-stats');
return (
stats ?? {
totalXp: 0,
totalSkills: 0,
highestLevel: 0,
streakDays: 0,
lastActivityDate: null,
}
);
}
export async function saveUserStats(stats: UserStats): Promise<void> {
const db = await getDB();
await db.put('stats', stats, 'user-stats');
}
// Utility: Recalculate stats from all skills
export async function recalculateStats(): Promise<UserStats> {
const skills = await getAllSkills();
const activities = await getAllActivities();
const stats: UserStats = {
totalXp: skills.reduce((sum, s) => sum + s.totalXp, 0),
totalSkills: skills.length,
highestLevel: skills.reduce((max, s) => Math.max(max, s.level), 0),
streakDays: calculateStreak(activities),
lastActivityDate: activities.length > 0 ? activities[activities.length - 1].timestamp : null,
};
await saveUserStats(stats);
return stats;
}
function calculateStreak(activities: Activity[]): number {
if (activities.length === 0) return 0;
const today = new Date();
today.setHours(0, 0, 0, 0);
const sortedDates = activities
.map((a) => {
const d = new Date(a.timestamp);
d.setHours(0, 0, 0, 0);
return d.getTime();
})
.filter((v, i, a) => a.indexOf(v) === i) // unique dates
.sort((a, b) => b - a); // newest first
let streak = 0;
let expectedDate = today.getTime();
for (const date of sortedDates) {
if (date === expectedDate || date === expectedDate - 86400000) {
streak++;
expectedDate = date - 86400000;
} else if (date < expectedDate - 86400000) {
break;
}
}
return streak;
}
// Export all data (for backup)
export async function exportData(): Promise<{
skills: Skill[];
activities: Activity[];
stats: UserStats;
}> {
const [skills, activities, stats] = await Promise.all([
getAllSkills(),
getAllActivities(),
getUserStats(),
]);
return { skills, activities, stats };
}
// Import data (restore backup)
export async function importData(data: {
skills: Skill[];
activities: Activity[];
stats: UserStats;
}): Promise<void> {
const db = await getDB();
const tx = db.transaction(['skills', 'activities', 'stats'], 'readwrite');
// Clear existing data
await tx.objectStore('skills').clear();
await tx.objectStore('activities').clear();
// Import new data
for (const skill of data.skills) {
await tx.objectStore('skills').put(skill);
}
for (const activity of data.activities) {
await tx.objectStore('activities').put(activity);
}
await tx.objectStore('stats').put(data.stats, 'user-stats');
await tx.done;
}
// Achievements CRUD
export async function getAllAchievements(): Promise<AchievementWithStatus[]> {
const db = await getDB();
return db.getAll('achievements');
}
export async function saveAchievement(achievement: AchievementWithStatus): Promise<void> {
const db = await getDB();
await db.put('achievements', achievement);
}
export async function saveAllAchievements(
achievementsList: AchievementWithStatus[]
): Promise<void> {
const db = await getDB();
const tx = db.transaction('achievements', 'readwrite');
await tx.objectStore('achievements').clear();
for (const a of achievementsList) {
await tx.objectStore('achievements').put(a);
}
await tx.done;
}
export async function unlockAchievement(id: string): Promise<void> {
const db = await getDB();
const achievement = await db.get('achievements', id);
if (achievement && !achievement.unlocked) {
achievement.unlocked = true;
achievement.unlockedAt = new Date().toISOString();
achievement.progress = achievement.condition.threshold;
await db.put('achievements', achievement);
}
}

View file

@ -1,3 +1,10 @@
/**
* Achievements Store Local-First with @manacore/local-store
*
* All achievement state stored in IndexedDB via Dexie.js.
* Sync to server happens automatically when authenticated.
*/
import type {
AchievementWithStatus,
AchievementUnlockResult,
@ -7,20 +14,18 @@ import type {
UserStats,
} from '$lib/types';
import { ACHIEVEMENT_DEFINITIONS } from '$lib/types';
import * as storage from '$lib/services/storage';
import * as achievementsApi from '$lib/api/achievements';
import { authStore } from './auth.svelte';
import { achievementCollection, type LocalAchievement } from '$lib/data/local-store';
// Reactive state
let achievements = $state<AchievementWithStatus[]>([]);
let isLoading = $state(true);
let initialized = $state(false);
let useApi = $state(false);
// Queue of recently unlocked achievements to show celebrations
let unlockQueue = $state<AchievementUnlockResult[]>([]);
// Derived values
// ─── Derived values ──────────────────────────────────────────
const unlockedAchievements = $derived(() => {
return achievements.filter((a) => a.unlocked);
});
@ -57,48 +62,55 @@ const completionPercentage = $derived(() => {
return Math.round((achievements.filter((a) => a.unlocked).length / achievements.length) * 100);
});
// Actions
// ─── Actions ─────────────────────────────────────────────────
async function initialize() {
if (initialized) return;
isLoading = true;
try {
if (authStore.isAuthenticated) {
useApi = true;
achievements = await achievementsApi.getAchievements();
} else {
useApi = false;
const stored = await storage.getAllAchievements();
if (stored.length === 0) {
// First time: seed from definitions
achievements = ACHIEVEMENT_DEFINITIONS.map((def) => ({
...def,
unlocked: false,
unlockedAt: null,
progress: 0,
}));
await storage.saveAllAchievements(achievements);
} else {
achievements = stored;
const stored = await achievementCollection.getAll();
if (stored.length === 0) {
// First time: seed from definitions
achievements = ACHIEVEMENT_DEFINITIONS.map((def) => ({
...def,
unlocked: false,
unlockedAt: null,
progress: 0,
}));
// Save each to IndexedDB
for (const a of achievements) {
await achievementCollection.insert({
id: a.id,
key: a.id,
name: a.name,
description: a.description,
icon: a.icon,
unlockedAt: '',
});
}
} else {
// Merge stored data with definitions (in case new achievements were added)
achievements = ACHIEVEMENT_DEFINITIONS.map((def) => {
const found = stored.find((s) => s.key === def.id || s.id === def.id);
return {
...def,
unlocked: found?.unlockedAt ? true : false,
unlockedAt: found?.unlockedAt || null,
progress: 0,
};
});
}
initialized = true;
} catch (error) {
console.error('Failed to initialize achievements store:', error);
// Fallback to local definitions
if (useApi) {
useApi = false;
const stored = await storage.getAllAchievements();
achievements =
stored.length > 0
? stored
: ACHIEVEMENT_DEFINITIONS.map((def) => ({
...def,
unlocked: false,
unlockedAt: null,
progress: 0,
}));
}
// Fallback to definitions
achievements = ACHIEVEMENT_DEFINITIONS.map((def) => ({
...def,
unlocked: false,
unlockedAt: null,
progress: 0,
}));
} finally {
isLoading = false;
}
@ -113,7 +125,7 @@ async function reinitialize() {
/**
* Check achievements locally (offline mode).
* Called after skill/activity changes when not using API.
* Called after skill/activity changes.
*/
async function checkLocal(context: {
skills: Skill[];
@ -200,14 +212,15 @@ async function checkLocal(context: {
progress: condition.threshold,
};
achievements = [...achievements.slice(0, i), unlocked, ...achievements.slice(i + 1)];
await storage.saveAchievement(unlocked);
await achievementCollection.update(a.id, {
unlockedAt: unlocked.unlockedAt!,
});
newlyUnlocked.push({ achievement: a, xpReward: a.xpReward });
} else {
// Update progress
const updated = { ...a, progress: Math.min(current, condition.threshold) };
if (updated.progress !== a.progress) {
achievements = [...achievements.slice(0, i), updated, ...achievements.slice(i + 1)];
await storage.saveAchievement(updated);
}
}
}
@ -220,7 +233,7 @@ async function checkLocal(context: {
}
/**
* Handle achievements returned from the API after a skill/XP action.
* Handle achievements returned from server sync.
*/
function handleApiUnlocks(results: AchievementUnlockResult[]) {
if (results.length === 0) return;
@ -279,9 +292,6 @@ export const achievementStore = {
get unlockQueue() {
return unlockQueue;
},
get useApi() {
return useApi;
},
initialize,
reinitialize,

View file

@ -1,10 +1,19 @@
import type { Skill, Activity, UserStats, SkillBranch, AchievementUnlockResult } from '$lib/types';
/**
* Skills Store Local-First with @manacore/local-store
*
* All reads and writes go to IndexedDB (Dexie.js) first.
* When authenticated, changes sync to the server in the background.
*/
import type { Skill, Activity, UserStats, SkillBranch } from '$lib/types';
import { calculateLevel, createDefaultSkill, createActivity, BRANCH_INFO } from '$lib/types';
import { SkillTreeEvents } from '@manacore/shared-utils/analytics';
import * as storage from '$lib/services/storage';
import * as skillsApi from '$lib/api/skills';
import * as activitiesApi from '$lib/api/activities';
import { authStore } from './auth.svelte';
import {
skillCollection,
activityCollection,
type LocalSkill,
type LocalActivity,
} from '$lib/data/local-store';
import { achievementStore } from './achievements.svelte';
// Reactive state using Svelte 5 runes
@ -19,9 +28,39 @@ let userStats = $state<UserStats>({
});
let isLoading = $state(true);
let initialized = $state(false);
let useApi = $state(false);
// Derived values
// ─── Converters ──────────────────────────────────────────────
function toSkill(local: LocalSkill): Skill {
return {
id: local.id,
name: local.name,
description: local.description,
branch: local.branch,
parentId: local.parentId ?? null,
icon: local.icon,
color: local.color ?? null,
currentXp: local.currentXp,
totalXp: local.totalXp,
level: local.level,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
function toActivity(local: LocalActivity): Activity {
return {
id: local.id,
skillId: local.skillId,
xpEarned: local.xpEarned,
description: local.description,
duration: local.duration ?? null,
timestamp: local.timestamp,
};
}
// ─── Derived values ──────────────────────────────────────────
const skillsByBranch = $derived(() => {
const grouped: Record<SkillBranch, Skill[]> = {
intellect: [],
@ -65,118 +104,78 @@ const branchStats = $derived(() => {
return stats;
});
// Actions
// ─── Actions ─────────────────────────────────────────────────
async function initialize() {
if (initialized) return;
isLoading = true;
try {
// Check if user is authenticated
if (authStore.isAuthenticated) {
useApi = true;
const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
skillsApi.getSkills(),
activitiesApi.getRecentActivities(50),
skillsApi.getStats(),
]);
skills = loadedSkills;
activities = loadedActivities;
userStats = loadedStats;
} else {
// Fallback to IndexedDB for offline/unauthenticated use
useApi = false;
const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
storage.getAllSkills(),
storage.getAllActivities(),
storage.getUserStats(),
]);
skills = loadedSkills;
activities = loadedActivities;
userStats = loadedStats;
}
const [localSkills, localActivities] = await Promise.all([
skillCollection.getAll(),
activityCollection.getAll(),
]);
skills = localSkills.map(toSkill);
activities = localActivities.map(toActivity);
recalculateStats();
initialized = true;
} catch (error) {
console.error('Failed to initialize skills store:', error);
// On error, try IndexedDB as fallback
if (useApi) {
try {
useApi = false;
const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
storage.getAllSkills(),
storage.getAllActivities(),
storage.getUserStats(),
]);
skills = loadedSkills;
activities = loadedActivities;
userStats = loadedStats;
} catch (fallbackError) {
console.error('Fallback to IndexedDB also failed:', fallbackError);
}
}
} finally {
isLoading = false;
}
}
async function addSkill(data: Partial<Skill>): Promise<Skill> {
if (useApi && authStore.isAuthenticated) {
const result = await skillsApi.createSkill({
name: data.name || '',
description: data.description,
branch: data.branch || 'custom',
parentId: data.parentId ?? undefined,
icon: data.icon,
color: data.color ?? undefined,
});
skills = [...skills, result.skill];
SkillTreeEvents.skillCreated(data.branch || 'custom');
await updateStats();
if (result.newAchievements?.length > 0) {
achievementStore.handleApiUnlocks(result.newAchievements);
}
return result.skill;
} else {
const skill = createDefaultSkill(data);
await storage.saveSkill(skill);
skills = [...skills, skill];
SkillTreeEvents.skillCreated(data.branch || 'custom');
await updateStats();
return skill;
}
const skill = createDefaultSkill(data);
const localSkill: LocalSkill = {
id: skill.id,
name: skill.name,
description: skill.description,
branch: skill.branch,
parentId: skill.parentId,
icon: skill.icon,
color: skill.color,
currentXp: skill.currentXp,
totalXp: skill.totalXp,
level: skill.level,
};
await skillCollection.insert(localSkill);
skills = [...skills, skill];
SkillTreeEvents.skillCreated(data.branch || 'custom');
recalculateStats();
return skill;
}
async function updateSkill(id: string, updates: Partial<Skill>): Promise<void> {
const index = skills.findIndex((s) => s.id === id);
if (index === -1) return;
if (useApi && authStore.isAuthenticated) {
const skill = await skillsApi.updateSkill(id, {
name: updates.name,
description: updates.description,
branch: updates.branch,
parentId: updates.parentId,
icon: updates.icon,
color: updates.color,
});
skills = [...skills.slice(0, index), skill, ...skills.slice(index + 1)];
} else {
const updatedSkill = { ...skills[index], ...updates, updatedAt: new Date().toISOString() };
await storage.saveSkill(updatedSkill);
skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
}
await updateStats();
const localUpdates: Partial<LocalSkill> = {};
if (updates.name !== undefined) localUpdates.name = updates.name;
if (updates.description !== undefined) localUpdates.description = updates.description;
if (updates.branch !== undefined) localUpdates.branch = updates.branch;
if (updates.parentId !== undefined) localUpdates.parentId = updates.parentId;
if (updates.icon !== undefined) localUpdates.icon = updates.icon;
if (updates.color !== undefined) localUpdates.color = updates.color;
await skillCollection.update(id, localUpdates);
const updatedSkill = { ...skills[index], ...updates, updatedAt: new Date().toISOString() };
skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
recalculateStats();
}
async function deleteSkill(id: string): Promise<void> {
if (useApi && authStore.isAuthenticated) {
await skillsApi.deleteSkill(id);
} else {
await storage.deleteSkill(id);
// Delete all activities for this skill
const skillActivities = await activityCollection.getAll({ skillId: id });
for (const a of skillActivities) {
await activityCollection.delete(a.id);
}
await skillCollection.delete(id);
skills = skills.filter((s) => s.id !== id);
SkillTreeEvents.skillDeleted();
activities = activities.filter((a) => a.skillId !== id);
await updateStats();
SkillTreeEvents.skillDeleted();
recalculateStats();
}
async function addXp(
@ -188,66 +187,89 @@ async function addXp(
const index = skills.findIndex((s) => s.id === skillId);
if (index === -1) return { leveledUp: false, newLevel: 0 };
if (useApi && authStore.isAuthenticated) {
const result = await skillsApi.addXp(skillId, { xp, description, duration });
skills = [...skills.slice(0, index), result.skill, ...skills.slice(index + 1)];
activities = [...activities, result.activity];
SkillTreeEvents.xpAdded(xp, result.leveledUp);
await updateStats();
if (result.newAchievements?.length > 0) {
achievementStore.handleApiUnlocks(result.newAchievements);
}
return { leveledUp: result.leveledUp, newLevel: result.newLevel };
} else {
const skill = skills[index];
const newTotalXp = skill.totalXp + xp;
const newCurrentXp = skill.currentXp + xp;
const newLevel = calculateLevel(newTotalXp);
const leveledUp = newLevel > skill.level;
const skill = skills[index];
const newTotalXp = skill.totalXp + xp;
const newCurrentXp = skill.currentXp + xp;
const newLevel = calculateLevel(newTotalXp);
const leveledUp = newLevel > skill.level;
const updatedSkill: Skill = {
...skill,
totalXp: newTotalXp,
currentXp: newCurrentXp,
level: newLevel,
updatedAt: new Date().toISOString(),
};
await skillCollection.update(skillId, {
totalXp: newTotalXp,
currentXp: newCurrentXp,
level: newLevel,
});
const activity = createActivity(skillId, xp, description, duration);
const activity = createActivity(skillId, xp, description, duration);
const localActivity: LocalActivity = {
id: activity.id,
skillId: activity.skillId,
xpEarned: activity.xpEarned,
description: activity.description,
duration: activity.duration,
timestamp: activity.timestamp,
};
await activityCollection.insert(localActivity);
await Promise.all([storage.saveSkill(updatedSkill), storage.saveActivity(activity)]);
const updatedSkill: Skill = {
...skill,
totalXp: newTotalXp,
currentXp: newCurrentXp,
level: newLevel,
updatedAt: new Date().toISOString(),
};
skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
activities = [...activities, activity];
await updateStats();
skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
activities = [...activities, activity];
SkillTreeEvents.xpAdded(xp, leveledUp);
recalculateStats();
return { leveledUp, newLevel };
}
return { leveledUp, newLevel };
}
async function updateStats(): Promise<void> {
if (useApi && authStore.isAuthenticated) {
try {
userStats = await skillsApi.getStats();
} catch {
// Calculate locally as fallback
userStats = calculateLocalStats();
}
} else {
userStats = await storage.recalculateStats();
}
}
function recalculateStats(): void {
const sortedActivities = [...activities].sort(
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
function calculateLocalStats(): UserStats {
return {
userStats = {
totalXp: skills.reduce((sum, s) => sum + s.totalXp, 0),
totalSkills: skills.length,
highestLevel: skills.reduce((max, s) => Math.max(max, s.level), 0),
streakDays: 0,
lastActivityDate: activities.length > 0 ? activities[activities.length - 1].timestamp : null,
streakDays: calculateStreak(activities),
lastActivityDate: sortedActivities.length > 0 ? sortedActivities[0].timestamp : null,
};
}
function calculateStreak(activityList: Activity[]): number {
if (activityList.length === 0) return 0;
const today = new Date();
today.setHours(0, 0, 0, 0);
const sortedDates = activityList
.map((a) => {
const d = new Date(a.timestamp);
d.setHours(0, 0, 0, 0);
return d.getTime();
})
.filter((v, i, a) => a.indexOf(v) === i)
.sort((a, b) => b - a);
let streak = 0;
let expectedDate = today.getTime();
for (const date of sortedDates) {
if (date === expectedDate || date === expectedDate - 86400000) {
streak++;
expectedDate = date - 86400000;
} else if (date < expectedDate - 86400000) {
break;
}
}
return streak;
}
function getSkill(id: string): Skill | undefined {
return skills.find((s) => s.id === id);
}
@ -256,7 +278,6 @@ function getSkillActivities(skillId: string): Activity[] {
return activities.filter((a) => a.skillId === skillId);
}
// Reinitialize when auth state changes
async function reinitialize() {
initialized = false;
skills = [];
@ -271,7 +292,7 @@ async function reinitialize() {
await initialize();
}
// Export store as object with getters for reactive access
// Export store
export const skillStore = {
get skills() {
return skills;
@ -300,9 +321,6 @@ export const skillStore = {
get branchStats() {
return branchStats;
},
get useApi() {
return useApi;
},
initialize,
reinitialize,

View file

@ -13,12 +13,12 @@
let { children } = $props();
async function handleAuthReady() {
// Initialize unified local-store (IndexedDB + sync)
// Initialize local-first database (IndexedDB via Dexie.js)
await skilltreeStore.initialize();
if (authStore.isAuthenticated) {
skilltreeStore.startSync(() => authStore.getValidToken());
}
// Initialize existing idb-based stores
// Load data from IndexedDB into reactive stores
await skillStore.initialize();
await achievementStore.initialize();
}