diff --git a/package.json b/package.json index 88f891b..dd15ce5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "ionicons": "^6.0.3", "jotai": "^1.8.6", "lodash": "^4.17.21", + "pouchdb": "^7.3.0", + "pouchdb-browser": "^7.3.0", + "pouchdb-find": "^7.3.0", "react": "^18.2.0", "react-cool-dimensions": "^2.0.7", "react-dom": "^18.2.0", @@ -33,6 +36,7 @@ "react-scripts": "^5.0.0", "typedoc": "^0.23.17", "typescript": "^4.1.3", + "use-pouchdb": "^1.4.0", "web-vitals": "^0.2.4", "workbox-background-sync": "^5.1.4", "workbox-broadcast-update": "^5.1.4", @@ -74,7 +78,8 @@ "devDependencies": { "@capacitor/cli": "4.3.0", "@ionic/cli": "6.20.3", - "@types/lodash": "^4.14.186" + "@types/lodash": "^4.14.186", + "@types/pouchdb": "^6.4.0" }, "description": "An Ionic project" } diff --git a/src/App.tsx b/src/App.tsx index cb0340a..43dec32 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,10 @@ import { geoPoint } from './components/map/types'; import Back from './components/buttons/Back'; import Forward from './components/buttons/Forward'; import CurrentLocation from './components/map/CurrentLocation'; +import { initDb } from './db'; +import PouchDB from 'pouchdb'; +import PouchDBFind from 'pouchdb-find'; +PouchDB.plugin(PouchDBFind); setupIonicReact(); @@ -83,6 +87,10 @@ export const setCenterAtom = atom(null, (get, set, center: geoPoint) => { }; set(scopeAtom, newScope); }); + +const db = new PouchDB('dyomedea', { auto_compaction: true, revs_limit: 10 }); +initDb(db); + /** * * @returns The root app component diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..87a2a6f --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,107 @@ +import _ from 'lodash'; +import uri from '../lib/ids'; + +const dbDefinitionId = uri('dbdef', {}); + +const currentDbDefinition = { + _id: dbDefinitionId, + type: dbDefinitionId, + def: { version: '0.000001' }, +}; + +export const initDb = async (db: any, setDbReady?: any) => { + var previousDbDefinition = { + _id: dbDefinitionId, + type: dbDefinitionId, + def: { version: '0' }, + }; + try { + previousDbDefinition = await db.get(dbDefinitionId); + } catch (error: any) { + if (error.status !== 404) { + console.log( + `Unexpected error fetching db definition: ${JSON.stringify(error)}` + ); + return; + } + } + + if (previousDbDefinition.def.version < currentDbDefinition.def.version) { + previousDbDefinition.def = currentDbDefinition.def; + db.put(previousDbDefinition); + // TODO: support migrations + } + await await db.compact(); + await db.viewCleanup(); + + // WARNING: defs must use the canonical form and be identical to what will be returned by db.getIndexes + const requiredIndexes: any = [ + { + name: 'type-subtype', + def: { + fields: [{ type: 'asc' }, { subtype: 'asc' }], + }, + }, + { + name: 'type-trkpt-gpx-time', + def: { + fields: [{ type: 'asc' }, { gpx: 'asc' }, { 'trkpt.time': 'asc' }], + }, + }, + ]; + + const existingIndexes = (await db.getIndexes()).indexes; + + const pruneIndex = ({ name, def }: any) => ({ name, def }); + + const isSameIndex = (idx1: any, idx2: any) => { + return _.isEqual(pruneIndex(idx1), pruneIndex(idx2)); + }; + + const findIndex = (targetIndexes: any, index: any) => + targetIndexes.find((targetIndex: any) => { + return isSameIndex(targetIndex, index); + }); + + for (var index of existingIndexes) { + if (index.type === 'json') { + // Non system indexes + // console.log(`Checking existing index :${JSON.stringify(index)}`); + if (!findIndex(requiredIndexes, index)) { + // console.log(`db.deleteIndex(${JSON.stringify(index)})`); + await db.deleteIndex(index); + } + } + } + + for (index of requiredIndexes) { + if (!findIndex(existingIndexes, index)) { + // console.log(`db.createIndex(${JSON.stringify(index)})`); + await db.createIndex({ name: index.name, ...index.def }); + } + } + + if (setDbReady !== undefined) setDbReady(true); + + /* const indexes = await db.getIndexes(); + console.log(`indexes: ${JSON.stringify(indexes)}`); + + const explain1 = await db.explain({ + selector: { + type: 'trkpt', + gpx: 'xxxx', + }, + // sort: ['trkpt.time'], + // use_index: 'type-trkpt-gpx-time', + }); + console.log(`explain1: ${JSON.stringify(explain1)}`); + const explain2 = await db.explain({ + selector: { + type: 'gpx', + }, + // sort: ['trkpt.time'], + // use_index: 'type-trkpt-gpx-time', + }); + console.log(`explain2: ${JSON.stringify(explain2)}`); + */ +}; diff --git a/src/lib/ids.ts b/src/lib/ids.ts new file mode 100644 index 0000000..57aca42 --- /dev/null +++ b/src/lib/ids.ts @@ -0,0 +1,21 @@ +import { route } from 'docuri'; + +const routes = { + dbdef: route('dbdef'), + settings: route('settings'), + gpx: route('gpx/:gpx'), + trk: route('gpx/:gpx/trk/:trk'), + trkseg: route('gpx/:gpx/trk/:trk/:trkseg'), + trkpt: route('gpx/:gpx/trk/:trk/:trkseg/:trkpt'), + wpt: route('gpx/:gpx/wpt/:wpt'), + rte: route('gpx/:gpx/rte/:rte'), + rtept: route('gpx/:gpx/rte/:rte/:rtept'), +}; + +type RouteKey = keyof typeof routes; + +const uri = (type: RouteKey, param: any) => { + return routes[type](param); +}; + +export default uri;