401 lines
11 KiB
JavaScript
401 lines
11 KiB
JavaScript
import React, {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useReducer,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
} from 'react';
|
|
import { useAppStatus } from './appStatus';
|
|
import {
|
|
getAllLocalPools,
|
|
savePoolLocal,
|
|
updatePoolLocal,
|
|
deletePoolLocal,
|
|
getContributionsForPool,
|
|
saveContributionLocal,
|
|
saveContributionsBatch,
|
|
getLatestContributionTimestamp,
|
|
deleteContributionsForPool,
|
|
} from '../app/functions/pools/poolsStorage';
|
|
import {
|
|
addPoolToDatabase,
|
|
updatePoolInDatabase,
|
|
getPoolFromDatabase,
|
|
getPoolsByCreator,
|
|
deletePoolFromDatabase,
|
|
getPoolContributionsSince,
|
|
} from '../db';
|
|
import { useGlobalContextProvider } from './context';
|
|
import { getLocalStorageItem, setLocalStorageItem } from '../app/functions';
|
|
import { STARTING_INDEX_FOR_POOLS_DERIVE } from '../app/constants';
|
|
|
|
const initialState = {
|
|
pools: {},
|
|
contributions: {},
|
|
};
|
|
|
|
const POOLS_DEBOUCE = 10000; //10s
|
|
|
|
function poolReducer(state, action) {
|
|
switch (action.type) {
|
|
case 'LOAD_LOCAL_POOLS':
|
|
return {
|
|
...state,
|
|
pools: action.payload.reduce((map, p) => {
|
|
map[p.poolId] = p;
|
|
return map;
|
|
}, {}),
|
|
};
|
|
|
|
case 'ADD_OR_UPDATE_POOL':
|
|
return {
|
|
...state,
|
|
pools: { ...state.pools, [action.payload.poolId]: action.payload },
|
|
};
|
|
|
|
case 'BULK_ADD_POOLS':
|
|
return {
|
|
...state,
|
|
pools: {
|
|
...state.pools,
|
|
...action.payload.reduce((map, p) => {
|
|
map[p.poolId] = p;
|
|
return map;
|
|
}, {}),
|
|
},
|
|
};
|
|
|
|
case 'LOAD_CONTRIBUTIONS':
|
|
return {
|
|
...state,
|
|
contributions: {
|
|
...state.contributions,
|
|
[action.payload.poolId]: action.payload.contributions,
|
|
},
|
|
};
|
|
|
|
case 'ADD_CONTRIBUTION': {
|
|
const poolId = action.payload.poolId;
|
|
const existing = state.contributions[poolId] || [];
|
|
return {
|
|
...state,
|
|
contributions: {
|
|
...state.contributions,
|
|
[poolId]: [action.payload, ...existing],
|
|
},
|
|
};
|
|
}
|
|
|
|
case 'BULK_ADD_CONTRIBUTIONS': {
|
|
const poolId = action.payload.poolId;
|
|
const existing = state.contributions[poolId] || [];
|
|
const existingIds = new Set(existing.map(c => c.contributionId));
|
|
const newOnes = action.payload.contributions.filter(
|
|
c => !existingIds.has(c.contributionId),
|
|
);
|
|
const merged = [...newOnes, ...existing].sort((a, b) => {
|
|
const aTime = Math.floor(a.createdAt / 1000) ?? 0;
|
|
const bTime = Math.floor(b.createdAt / 1000) ?? 0;
|
|
return bTime - aTime;
|
|
});
|
|
return {
|
|
...state,
|
|
contributions: {
|
|
...state.contributions,
|
|
[poolId]: merged,
|
|
},
|
|
};
|
|
}
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
const PoolContext = createContext(null);
|
|
|
|
export function PoolProvider({ children }) {
|
|
const { masterInfoObject, toggleMasterInfoObject } =
|
|
useGlobalContextProvider();
|
|
const { didGetToHomepage } = useAppStatus();
|
|
const [state, dispatch] = useReducer(poolReducer, initialState);
|
|
const isRestoring = useRef(false);
|
|
const userUuidRef = useRef(masterInfoObject?.uuid);
|
|
const poolUpdateTracker = useRef({});
|
|
userUuidRef.current = masterInfoObject?.uuid;
|
|
|
|
const updatePoolList = useCallback(async () => {
|
|
const updatedList = await getAllLocalPools();
|
|
dispatch({ type: 'LOAD_LOCAL_POOLS', payload: updatedList });
|
|
return updatedList;
|
|
}, []);
|
|
|
|
const savePoolToCloud = async poolObj => {
|
|
try {
|
|
const serverResponse = await addPoolToDatabase(poolObj);
|
|
|
|
if (!serverResponse) {
|
|
throw new Error('Server save failed');
|
|
}
|
|
const localObject = JSON.parse(JSON.stringify(poolObj));
|
|
const localResponse = await savePoolLocal(localObject);
|
|
|
|
if (!localResponse) {
|
|
throw new Error('Local save failed');
|
|
}
|
|
dispatch({ type: 'ADD_OR_UPDATE_POOL', payload: localObject });
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error saving pool to cloud', err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const updatePool = async poolObj => {
|
|
try {
|
|
await updatePoolLocal(poolObj.poolId, poolObj);
|
|
await updatePoolInDatabase(poolObj);
|
|
dispatch({ type: 'ADD_OR_UPDATE_POOL', payload: poolObj });
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error updating pool', err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const deletePool = async poolId => {
|
|
try {
|
|
await deletePoolLocal(poolId);
|
|
await deleteContributionsForPool(poolId);
|
|
await deletePoolFromDatabase(poolId);
|
|
await updatePoolList();
|
|
return true;
|
|
} catch (err) {
|
|
console.log('error deleting pool', err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Sync active pools from Firestore for latest aggregates
|
|
const syncActivePoolsFromServer = useCallback(async localPools => {
|
|
try {
|
|
const activePools = localPools.filter(p => p.status === 'active');
|
|
if (!activePools.length) {
|
|
console.warn('No active pools');
|
|
return;
|
|
}
|
|
|
|
const refreshed = await Promise.all(
|
|
activePools.map(pool => getPoolFromDatabase(pool.poolId)),
|
|
);
|
|
|
|
const now = Date.now();
|
|
const updates = refreshed.filter(Boolean);
|
|
if (updates.length) {
|
|
await Promise.all(
|
|
updates.map(pool => {
|
|
poolUpdateTracker.current[pool.poolId] = now;
|
|
return savePoolLocal(pool);
|
|
}),
|
|
);
|
|
dispatch({ type: 'BULK_ADD_POOLS', payload: updates });
|
|
}
|
|
} catch (err) {
|
|
console.log('error syncing active pools from server', err);
|
|
}
|
|
}, []);
|
|
|
|
// Restore pools from Firestore on new device / reinstall
|
|
const handlePoolRestore = async (localPools, isActive = () => true) => {
|
|
try {
|
|
if (isRestoring.current) return;
|
|
isRestoring.current = true;
|
|
|
|
// If we already have local pools, just sync latest state
|
|
if (localPools?.length) {
|
|
return;
|
|
}
|
|
|
|
// Check if we already attempted restore
|
|
const didCheckForPools = JSON.parse(
|
|
await getLocalStorageItem('checkForOutstandingPools'),
|
|
);
|
|
if (didCheckForPools) return;
|
|
|
|
// Query Firestore for all pools created by this user
|
|
const serverPools = await getPoolsByCreator(masterInfoObject.uuid);
|
|
|
|
if (!serverPools.length) {
|
|
await setLocalStorageItem(
|
|
'checkForOutstandingPools',
|
|
JSON.stringify(true),
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log(`Restoring ${serverPools.length} pools from server`);
|
|
|
|
// Save all server pools to local SQLite
|
|
await Promise.all(serverPools.map(pool => savePoolLocal(pool)));
|
|
|
|
// Update currentDerivedPoolIndex to max found to prevent collisions
|
|
const maxDerivationIndex = Math.max(
|
|
...serverPools.map(p => p.derivationIndex || 0),
|
|
0,
|
|
);
|
|
const restoredPoolCount =
|
|
maxDerivationIndex - STARTING_INDEX_FOR_POOLS_DERIVE + 1;
|
|
if (restoredPoolCount > (masterInfoObject.currentDerivedPoolIndex || 0)) {
|
|
toggleMasterInfoObject({
|
|
currentDerivedPoolIndex: restoredPoolCount,
|
|
});
|
|
}
|
|
|
|
if (!isActive()) return;
|
|
dispatch({ type: 'BULK_ADD_POOLS', payload: serverPools });
|
|
await setLocalStorageItem(
|
|
'checkForOutstandingPools',
|
|
JSON.stringify(true),
|
|
);
|
|
} catch (err) {
|
|
console.log('error restoring pools', err);
|
|
} finally {
|
|
isRestoring.current = false;
|
|
}
|
|
};
|
|
|
|
const loadContributionsForPool = useCallback(async poolId => {
|
|
try {
|
|
const cached = await getContributionsForPool(poolId);
|
|
dispatch({
|
|
type: 'LOAD_CONTRIBUTIONS',
|
|
payload: { poolId, contributions: cached },
|
|
});
|
|
return cached;
|
|
} catch (err) {
|
|
console.log('Error loading contributions for pool:', err);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
const syncPool = useCallback(async (poolId, forceSymc = false) => {
|
|
try {
|
|
const latestTimestamp = await getLatestContributionTimestamp(poolId);
|
|
const newContributions = await getPoolContributionsSince(
|
|
poolId,
|
|
latestTimestamp,
|
|
);
|
|
|
|
let freshPool;
|
|
const now = Date.now();
|
|
|
|
if (!poolUpdateTracker.current[poolId]) {
|
|
poolUpdateTracker.current[poolId] = now;
|
|
}
|
|
if (
|
|
now - poolUpdateTracker.current[poolId] > POOLS_DEBOUCE ||
|
|
newContributions.length > 0 ||
|
|
forceSymc
|
|
) {
|
|
freshPool = await getPoolFromDatabase(poolId);
|
|
if (freshPool) {
|
|
const isOwnPool = freshPool.creatorUUID === userUuidRef.current;
|
|
// Only persist to SQLite if this is the user's own pool
|
|
if (isOwnPool) {
|
|
await savePoolLocal(freshPool);
|
|
}
|
|
dispatch({ type: 'ADD_OR_UPDATE_POOL', payload: freshPool });
|
|
}
|
|
poolUpdateTracker.current[poolId] = now;
|
|
} else {
|
|
console.warn('Not getting pool from database too soon', poolId);
|
|
}
|
|
|
|
if (newContributions.length > 0) {
|
|
await saveContributionsBatch(newContributions);
|
|
dispatch({
|
|
type: 'BULK_ADD_CONTRIBUTIONS',
|
|
payload: { poolId, contributions: newContributions },
|
|
});
|
|
}
|
|
return !!freshPool || newContributions.length > 0;
|
|
} catch (err) {
|
|
console.log('Error syncing pool:', err);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
const addContributionToCache = useCallback(async contribution => {
|
|
try {
|
|
await saveContributionLocal(contribution);
|
|
dispatch({ type: 'ADD_CONTRIBUTION', payload: contribution });
|
|
} catch (err) {
|
|
console.log('Error adding contribution to cache:', err);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!didGetToHomepage) return;
|
|
let active = true;
|
|
(async () => {
|
|
const allPools = await getAllLocalPools();
|
|
if (!active) return;
|
|
dispatch({ type: 'LOAD_LOCAL_POOLS', payload: allPools });
|
|
if (!active) return;
|
|
await handlePoolRestore(allPools, () => active);
|
|
})();
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [didGetToHomepage]);
|
|
|
|
const { poolsArray, activePoolsArray, closedPoolsArray } = useMemo(() => {
|
|
const uuid = masterInfoObject?.uuid;
|
|
// Only show pools the user created in the list arrays
|
|
const ownedPools = Object.values(state.pools).filter(
|
|
pool => pool.creatorUUID === uuid,
|
|
);
|
|
|
|
const active = ownedPools
|
|
.filter(pool => pool.status === 'active')
|
|
.sort((a, b) => b.createdAt - a.createdAt);
|
|
|
|
const closed = ownedPools
|
|
.filter(pool => pool.status === 'closed')
|
|
.sort(
|
|
(a, b) => (b.closedAt || b.createdAt) - (a.closedAt || a.createdAt),
|
|
);
|
|
|
|
return {
|
|
poolsArray: ownedPools.sort((a, b) => b.createdAt - a.createdAt),
|
|
activePoolsArray: active,
|
|
closedPoolsArray: closed,
|
|
};
|
|
}, [state.pools, masterInfoObject?.uuid]);
|
|
|
|
return (
|
|
<PoolContext.Provider
|
|
value={{
|
|
...state,
|
|
poolsArray,
|
|
activePoolsArray,
|
|
closedPoolsArray,
|
|
savePoolToCloud,
|
|
updatePool,
|
|
deletePool,
|
|
updatePoolList,
|
|
syncActivePoolsFromServer,
|
|
loadContributionsForPool,
|
|
syncPool,
|
|
addContributionToCache,
|
|
}}
|
|
>
|
|
{children}
|
|
</PoolContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const usePools = () => useContext(PoolContext);
|