Migrate from .NET MAUI to Expo + Convex
Complete rewrite of Scry using TypeScript stack:
- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)
Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support
Removes all .NET/MAUI code (src/, test/, tools/).
💘 Generated with Crush
Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
This commit is contained in:
parent
56499d5af9
commit
83ab4df537
138 changed files with 19136 additions and 7681 deletions
183
lib/db/syncService.ts
Normal file
183
lib/db/syncService.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/**
|
||||
* Sync service for keeping local SQLite cache in sync with Convex.
|
||||
* Provides offline-first functionality with background sync.
|
||||
*/
|
||||
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import {
|
||||
initDatabase,
|
||||
getLastSyncTimestamp,
|
||||
setLastSyncTimestamp,
|
||||
getCachedHashes,
|
||||
getCachedCardCount,
|
||||
upsertCards,
|
||||
clearCache,
|
||||
} from "./localDatabase";
|
||||
import type { CardHashEntry } from "../recognition";
|
||||
|
||||
export interface SyncStatus {
|
||||
isInitialized: boolean;
|
||||
isSyncing: boolean;
|
||||
lastSync: number;
|
||||
localCardCount: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SyncService {
|
||||
initialize: () => Promise<void>;
|
||||
sync: () => Promise<void>;
|
||||
getHashes: () => Promise<CardHashEntry[]>;
|
||||
getStatus: () => SyncStatus;
|
||||
clearLocalCache: () => Promise<void>;
|
||||
onStatusChange: (callback: (status: SyncStatus) => void) => () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync service instance.
|
||||
*/
|
||||
export function createSyncService(convexClient: ConvexReactClient): SyncService {
|
||||
let status: SyncStatus = {
|
||||
isInitialized: false,
|
||||
isSyncing: false,
|
||||
lastSync: 0,
|
||||
localCardCount: 0,
|
||||
};
|
||||
|
||||
const listeners = new Set<(status: SyncStatus) => void>();
|
||||
|
||||
function notifyListeners() {
|
||||
listeners.forEach((cb) => cb(status));
|
||||
}
|
||||
|
||||
function updateStatus(partial: Partial<SyncStatus>) {
|
||||
status = { ...status, ...partial };
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Initialize the local database and load cached data.
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
await initDatabase();
|
||||
|
||||
const [lastSync, cardCount] = await Promise.all([
|
||||
getLastSyncTimestamp(),
|
||||
getCachedCardCount(),
|
||||
]);
|
||||
|
||||
updateStatus({
|
||||
isInitialized: true,
|
||||
lastSync,
|
||||
localCardCount: cardCount,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
console.log(`[SyncService] Initialized with ${cardCount} cached cards`);
|
||||
} catch (error) {
|
||||
console.error("[SyncService] Initialization failed:", error);
|
||||
updateStatus({
|
||||
error: error instanceof Error ? error.message : "Initialization failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync with Convex - fetch new/updated cards.
|
||||
*/
|
||||
async sync() {
|
||||
if (status.isSyncing) {
|
||||
console.log("[SyncService] Sync already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus({ isSyncing: true, error: undefined });
|
||||
|
||||
try {
|
||||
// Get cards updated since last sync
|
||||
const lastSync = status.lastSync;
|
||||
|
||||
// Query Convex for updated cards
|
||||
// Note: This uses the HTTP client for one-off queries
|
||||
const cards = lastSync > 0
|
||||
? await convexClient.query(api.cards.listUpdatedAfter, { since: lastSync })
|
||||
: await convexClient.query(api.cards.list, {});
|
||||
|
||||
if (cards && cards.length > 0) {
|
||||
// Convert to local format and save
|
||||
const localCards = cards.map((card) => ({
|
||||
scryfallId: card.scryfallId,
|
||||
oracleId: card.oracleId,
|
||||
name: card.name,
|
||||
setCode: card.setCode,
|
||||
collectorNumber: card.collectorNumber,
|
||||
rarity: card.rarity,
|
||||
artist: card.artist,
|
||||
imageUri: card.imageUri,
|
||||
hash: new Uint8Array(card.hash),
|
||||
hashVersion: card.hashVersion,
|
||||
updatedAt: card.updatedAt,
|
||||
}));
|
||||
|
||||
await upsertCards(localCards);
|
||||
console.log(`[SyncService] Synced ${localCards.length} cards`);
|
||||
}
|
||||
|
||||
// Update sync timestamp
|
||||
const now = Date.now();
|
||||
await setLastSyncTimestamp(now);
|
||||
|
||||
// Update status
|
||||
const cardCount = await getCachedCardCount();
|
||||
updateStatus({
|
||||
isSyncing: false,
|
||||
lastSync: now,
|
||||
localCardCount: cardCount,
|
||||
});
|
||||
|
||||
console.log(`[SyncService] Sync complete. ${cardCount} cards cached.`);
|
||||
} catch (error) {
|
||||
console.error("[SyncService] Sync failed:", error);
|
||||
updateStatus({
|
||||
isSyncing: false,
|
||||
error: error instanceof Error ? error.message : "Sync failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cached hashes for recognition.
|
||||
*/
|
||||
async getHashes() {
|
||||
return getCachedHashes();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current sync status.
|
||||
*/
|
||||
getStatus() {
|
||||
return status;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear local cache and re-sync.
|
||||
*/
|
||||
async clearLocalCache() {
|
||||
await clearCache();
|
||||
updateStatus({
|
||||
lastSync: 0,
|
||||
localCardCount: 0,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribe to status changes.
|
||||
*/
|
||||
onStatusChange(callback: (status: SyncStatus) => void) {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue