import { createEffect, createRoot, createSignal } from 'solid-js'; import PouchDB from 'pouchdb'; import indexeddb from 'pouchdb-adapter-indexeddb'; import { sleep, until } from '../lib/async-wait'; import { isEqual } from 'lodash'; PouchDB.plugin(indexeddb); declare global { var localDb: any; var db: any; var remoteDb: any; var currentAccount: any; var sync: any; } const [state, setState] = createSignal({}); export { state }; export const watchDbLegacy = async () => { createRoot(async (dispose) => { console.log({ caller: 'watchDbLegacy / start' }); const { db, localDb, remoteDb, sync, currentAccount } = globalThis; const [status, setStatus] = createSignal({}); const [syncState, setSyncState] = createSignal({}); const [remoteUrl, setRemoteUrl] = createSignal(); const updateStatus = async () => { const dbInfo = await db.info(); const localDbInfo = await localDb.info(); const remoteDbInfo = remoteDb ? await remoteDb.info() : undefined; const tasks = PouchDB.activeTasks.list(); const newStatus = { ...status(), tasks, dbInfo, localDbInfo, remoteDbInfo, // date: new Date(), }; if (!isEqual(status(), newStatus)) { setStatus(newStatus); } }; createEffect(() => { console.log({ caller: 'watchDbLegacy', state: state(), status: status(), syncState: syncState(), sync, }); }); createEffect(() => { const newState = { ...state(), dbName: status()?.dbInfo?.db_name, localUpdateSeq: status()?.dbInfo?.update_seq, remoteUrl: status()?.remoteDbInfo?.host, sync: syncState(), }; if (!isEqual(newState, state())) { setState(newState); } }); updateStatus(); if (sync) { sync .on('change', function (info) { // handle change // console.log({ caller: 'Sync / change', info }); if (info.direction === 'push') { setSyncState({ ...syncState(), lastSeq: info.change.last_seq }); } }) .on('paused', function (err) { // replication paused (e.g. replication up to date, user went offline) setSyncState({ ...syncState(), paused: true }); // console.log({ caller: 'Sync / paused', err }); }) .on('active', function () { // replicate resumed (e.g. new changes replicating, user went back online) setSyncState({ ...syncState(), paused: false }); // console.log({ caller: 'Sync / active' }); }) .on('denied', function (err) { // a document failed to replicate (e.g. due to permissions) console.error({ caller: 'Sync / denied', err }); }) .on('complete', function (info) { // handle complete console.log({ caller: 'Sync / complete', info }); }) .on('error', function (err) { // handle error console.error({ caller: 'Sync / error', err }); }); } const timerId = setInterval(updateStatus, 15000); const findDocumentsWithoutOrigin = async () => { const allDocs = await db.allDocs({ include_docs: true }); return allDocs.rows .filter((row: any) => !row.doc.origin) .map((row: any) => row.doc._id); }; createEffect(async () => { if (!!state().remoteUrl && remoteUrl() !== state().remoteUrl) { setRemoteUrl(state().remoteUrl); // Check documents and update without origin const documentsWithoutOrigin = await findDocumentsWithoutOrigin(); console.log({ caller: 'watchDbLegacy / addOrigin', remoteUrl: remoteUrl(), nbDocumentsWithoutOrigin: documentsWithoutOrigin.length, }); for (const docId of documentsWithoutOrigin) { while (true) { await sleep(100); const doc = await db.get(docId); console.log({ caller: 'watchDbLegacy / addOrigin', remoteUrl: remoteUrl(), docId, doc, }); try { db.put({ ...doc, origin: remoteUrl() }); break; } catch (error) { if (error.name === 'conflict') { console.log({ caller: 'watchDbLegacy / addOrigin', docId, doc, error, }); } else { console.error({ caller: 'watchDbLegacy / addOrigin', docId, doc, error, }); break; } } } } } }); // db.compact(); const openIDb = (name: string) => new Promise((resolve, reject) => { const iDb = indexedDB.open(name); iDb.onerror = (event: any) => { console.error({ caller: 'watchDbLegacy', message: 'open db error', target: event.target, }); reject(event.target.errorCode); }; iDb.onsuccess = (event: any) => { resolve(event.target.result); }; }); const getAll = (store: any) => new Promise((resolve, reject) => { const request = store.getAll(); request.onerror = (event: any) => { console.error({ caller: 'watchDbLegacy', message: 'getAll error', target: event.target, }); reject(event.target.errorCode); }; request.onsuccess = (event: any) => { resolve(event.target.result as any[]); }; }); const getDeletedDocs = async () => { const iDb = (await openIDb(`_pouch_${state().dbName}`)) as IDBDatabase; const bySequence = iDb.transaction('docs', 'readonly'); const store = bySequence.objectStore('docs'); const allDocs = (await getAll(store)) as any[]; iDb.close(); const docs = allDocs .filter( (doc) => !doc.id.startsWith('_local') && doc.deleted === 1 && doc.seq <= state()?.sync?.lastSeq ) .map((doc) => ({ id: doc.id, rev: doc.rev })); // console.log({ // caller: 'watchDbLegacy / getDeletedDocs', // // bySequence, // // store, // // allDocs, // docs, // state: state(),gi // }); return docs; }; while (true) { await until( () => state()?.sync?.paused && state()?.sync?.lastSeq >= state()?.localUpdateSeq - 1, // I wonder why this can happen ! 1000 ); console.log({ caller: 'watchDbLegacy / ready to purge after sync ' }); try { const localPurges = await db.get('_local/purges'); localPurges._deleted = true; await db.put(localPurges); } catch (error) { // console.warn({ caller: 'watchDbLegacy / _local/purges', error }); } const deletedDocs = await getDeletedDocs(); let i = 0; if (deletedDocs.length > 0) { console.log({ caller: 'watchDbLegacy / purge', // bySequence, // store, // allDocs, deletedDocs, state: state(), }); for (const doc of deletedDocs) { await sleep(10); try { await db.purge(doc.id, doc.rev); } catch (err) { console.log({ caller: 'watchDbLegacy / purge', doc, err, }); } i = i + 1; if (i % 10 === 0) { console.log({ caller: 'watchDbLegacy / purge', nbDeletedDocs: i, nbDocsToDelete: deletedDocs.length, doc, }); } if (i === 1000) { break; } } } console.log({ caller: 'watchDbLegacy / purge', nbDeletedDocs: i, nbDocsToDelete: deletedDocs.length, }); if (i === deletedDocs.length) { await sleep(60000); } } }); };