syncing channels to store

This commit is contained in:
balzack 2022-09-15 01:03:20 -07:00
parent 33966512f1
commit 1abe9d197b
12 changed files with 318 additions and 35 deletions

View File

@ -9,11 +9,15 @@ import { StoreContextProvider } from 'context/StoreContext';
import { AppContextProvider } from 'context/AppContext';
import { AccountContextProvider } from 'context/AccountContext';
import { ProfileContextProvider } from 'context/ProfileContext';
import { CardContextProvider } from 'context/CardContext';
import { ChannelContextProvider } from 'context/ChannelContext';
export default function App() {
return (
<StoreContextProvider>
<CardContextProvider>
<ChannelContextProvider>
<AccountContextProvider>
<ProfileContextProvider>
<AppContextProvider>
@ -30,6 +34,8 @@ export default function App() {
</AppContextProvider>
</ProfileContextProvider>
</AccountContextProvider>
</ChannelContextProvider>
</CardContextProvider>
</StoreContextProvider>
);
}

View File

@ -1,7 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function getChannelDetail(token, channelId) {
let detail = await fetchWithTimeout(`/content/channels/${channelId}/detail?agent=${token}`, { method: 'GET' });
export async function getChannelDetail(server, token, channelId) {
let detail = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/detail?agent=${token}`, { method: 'GET' });
checkResponse(detail)
return await detail.json()
}

View File

@ -1,7 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function getChannelSummary(token, channelId) {
let summary = await fetchWithTimeout(`/content/channels/${channelId}/summary?agent=${token}`, { method: 'GET' });
export async function getChannelSummary(server, token, channelId) {
let summary = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/summary?agent=${token}`, { method: 'GET' });
checkResponse(summary)
return await summary.json()
}

View File

@ -1,11 +1,11 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function getChannels(token, revision) {
export async function getChannels(server, token, revision) {
let param = "?agent=" + token
if (revision != null) {
param += '&channelRevision=' + revision
}
let channels = await fetchWithTimeout('/content/channels' + param, { method: 'GET' });
let channels = await fetchWithTimeout(`https://${server}/content/channels${param}`, { method: 'GET' });
checkResponse(channels)
let ret = await channels.json()
return ret;

View File

@ -0,0 +1,14 @@
import { createContext } from 'react';
import { useCardContext } from './useCardContext.hook';
export const CardContext = createContext({});
export function CardContextProvider({ children }) {
const { state, actions } = useCardContext();
return (
<CardContext.Provider value={{ state, actions }}>
{children}
</CardContext.Provider>
);
}

View File

@ -0,0 +1,14 @@
import { createContext } from 'react';
import { useChannelContext } from './useChannelContext.hook';
export const ChannelContext = createContext({});
export function ChannelContextProvider({ children }) {
const { state, actions } = useChannelContext();
return (
<ChannelContext.Provider value={{ state, actions }}>
{children}
</ChannelContext.Provider>
);
}

View File

@ -34,6 +34,8 @@ export function useAccountContext() {
}
catch(err) {
console.log(err);
syncing.current = false;
return;
}
syncing.current = false;

View File

@ -7,6 +7,7 @@ import { getUsername } from 'api/getUsername';
import { StoreContext } from 'context/StoreContext';
import { AccountContext } from 'context/AccountContext';
import { ProfileContext } from 'context/ProfileContext';
import { ChannelContext } from 'context/ChannelContext';
export function useAppContext() {
const [state, setState] = useState({
@ -16,6 +17,7 @@ export function useAppContext() {
const store = useContext(StoreContext);
const account = useContext(AccountContext);
const profile = useContext(ProfileContext);
const channel = useContext(ChannelContext);
const delay = useRef(2);
const ws = useRef(null);
@ -41,6 +43,7 @@ export function useAppContext() {
const setSession = async (access) => {
await account.actions.setSession(access);
await profile.actions.setSession(access);
await channel.actions.setSession(access);
updateState({ session: true });
setWebsocket(access.server, access.appToken);
}
@ -48,6 +51,7 @@ export function useAppContext() {
const clearSession = async () => {
account.actions.clearSession();
profile.actions.clearSession();
channel.actions.clearSession();
updateState({ session: false });
clearWebsocket();
}
@ -84,8 +88,14 @@ export function useAppContext() {
ws.current.onmessage = (ev) => {
try {
const rev = JSON.parse(ev.data);
try {
profile.actions.setRevision(rev.profile);
account.actions.setRevision(rev.account);
channel.actions.setRevision(rev.channel);
}
catch(err) {
console.log(err);
}
updateState({ disconnected: false });
}
catch (err) {

View File

@ -0,0 +1,68 @@
import { useState, useRef, useContext } from 'react';
import { StoreContext } from 'context/StoreContext';
export function useCardContext() {
const [state, setState] = useState({
});
const store = useContext(StoreContext);
const session = useRef(null);
const curRevision = useRef(null);
const setRevision = useRef(null);
const syncing = useRef(false);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }))
}
const sync = async () => {
if (!syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
try {
const revision = curRevision.current;
const { server, appToken, guid } = session.current;
// get and store
updateState({ status });
setRevision.current = revision;
}
catch(err) {
console.log(err);
syncing.current = false;
return;
}
syncing.current = false;
sync();
}
};
const actions = {
setSession: async (access) => {
const { guid, server, appToken } = access;
// load
const revision = await store.actions.getCardRevision(guid);
// update
setRevision.current = revision;
curRevision.current = revision;
session.current = access;
},
clearSession: () => {
session.current = {};
updateState({ account: null });
},
setRevision: (rev) => {
curRevision.current = rev;
sync();
},
}
return { state, actions }
}

View File

@ -0,0 +1,95 @@
import { useState, useRef, useContext } from 'react';
import { StoreContext } from 'context/StoreContext';
import { getChannels } from 'api/getChannels';
import { getChannelDetail } from 'api/getChannelDetail';
import { getChannelSummary } from 'api/getChannelSummary';
export function useChannelContext() {
const [state, setState] = useState({
});
const store = useContext(StoreContext);
const session = useRef(null);
const curRevision = useRef(null);
const setRevision = useRef(null);
const syncing = useRef(false);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }))
}
const sync = async () => {
if (!syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
try {
const revision = curRevision.current;
const { server, appToken, guid } = session.current;
const delta = await getChannels(server, appToken, setRevision.current);
for (let channel of delta) {
if (channel.data) {
if (channel.data.channelDetail && channel.data.channelSummary) {
await store.actions.setChannelItem(guid, channel);
}
else {
const { detailRevision, topicRevision, channelDetail, channelSummary } = channel.data;
const view = await store.actions.getChannelItemView(guid, channel.id);
if (view.detailRevision != detailRevision) {
const detail = await getChannelDetail(server, appToken, channel.id);
await store.actions.setChannelItemDetail(guid, channel.id, detailRevision, detail);
}
if (view.topicRevision != topicRevision) {
const summary = await getChannelSummary(server, appToken, channel.id);
await store.actions.setChannelItemSummary(guid, channel.id, topicRevision, summary);
}
await store.actions.setChannelItemRevision(guid, channel.revision);
}
}
else {
await store.actions.clearChannelItem(channel.id);
}
}
setRevision.current = revision;
await store.actions.setChannelRevision(guid, revision);
}
catch(err) {
console.log(err);
syncing.current = false;
return;
}
syncing.current = false;
sync();
}
};
const actions = {
setSession: async (access) => {
const { guid, server, appToken } = access;
// load
const revision = await store.actions.getChannelRevision(guid);
// update
setRevision.current = revision;
curRevision.current = revision;
session.current = access;
},
clearSession: () => {
session.current = {};
updateState({ account: null });
},
setRevision: (rev) => {
curRevision.current = rev;
sync();
},
}
return { state, actions }
}

View File

@ -36,6 +36,8 @@ export function useProfileContext() {
}
catch(err) {
console.log(err);
syncing.current = false;
return;
}
syncing.current = false;

View File

@ -1,7 +1,7 @@
import { useEffect, useState, useRef, useContext } from 'react';
import SQLite from "react-native-sqlite-storage";
const DATABAG_DB = 'databag_v005.db';
const DATABAG_DB = 'databag_v011.db';
export function useStoreContext() {
const [state, setState] = useState({});
@ -11,6 +11,11 @@ export function useStoreContext() {
setState((s) => ({ ...s, ...value }))
}
const initSession = async (guid) => {
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, detail text, summary text, unique(channel_id))`);
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS topic_${guid} (channel_id text, topic_id text, revision integer, detail_revision integer, detail text, unique(channel_id, topic_id))`);
}
const actions = {
init: async () => {
SQLite.DEBUG(false);
@ -21,6 +26,7 @@ export function useStoreContext() {
return await getAppValue(db.current, 'session');
},
setSession: async (access) => {
await initSession(access.guid);
await db.current.executeSql("UPDATE app SET value=? WHERE key='session';", [encodeObject(access)]);
},
clearSession: async () => {
@ -33,17 +39,15 @@ export function useStoreContext() {
},
setProfile: async (guid, profile) => {
const dataId = `${guid}_profile`;
await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, null);", [dataId]);
await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(profile), dataId]);
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(profile)]);
},
getProfileRevision: async (guid) => {
const dataId = `${guid}_profileRevision`;
return await getAppValue(db.current, dataId, 0);
return await getAppValue(db.current, dataId, null);
},
setProfileRevision: async (guid, revision) => {
const dataId = `${guid}_profileRevision`;
await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, 0);", [dataId]);
await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(revision), dataId]);
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]);
},
getAccountStatus: async (guid) => {
@ -52,17 +56,72 @@ export function useStoreContext() {
},
setAccountStatus: async (guid, status) => {
const dataId = `${guid}_status`;
await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, null);", [dataId]);
await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(status), dataId]);
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(status)]);
},
getAccountRevision: async (guid) => {
const dataId = `${guid}_accountRevision`;
return await getAppValue(db.current, dataId, 0);
return await getAppValue(db.current, dataId, null);
},
setAccountRevision: async (guid, revision) => {
const dataId = `${guid}_accountRevision`;
await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, 0);", [dataId]);
await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(revision), dataId]);
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]);
},
getCardRevision: async (guid) => {
const dataId = `${guid}_cardRevision`;
return await getAppValue(db.current, dataId, null);
},
setCardRevision: async (guid, revision) => {
const dataId = `${guid}_cardRevision`;
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]);
},
getChannelRevision: async (guid) => {
const dataId = `${guid}_channelRevision`;
return await getAppValue(db.current, dataId, null);
},
setChannelRevision: async (guid, revision) => {
const dataId = `${guid}_channelRevision`;
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]);
},
setChannelItem: async (guid, channel) => {
const { id, revision, data } = channel;
await db.current.executeSql(`INSERT OR REPLACE INTO channel_${guid} (channel_id, revision, detail_revision, topic_revision, detail, summary) values (?, ?, ?, ?, ?, ?);`, [id, revision, data.detailRevision, data.topicRevision, encodeObject(data.channelDetail), encodeObject(data.channelSummary)]);
},
clearChannelItem: async (guid, channelId) => {
await db.current.executeSql(`DELETE FROM channel_${guid} WHERE channel_id=?`, [channelId]);
},
setChannelItemRevision: async (guid, channelId, revision) => {
await db.current.executeSql(`UPDATE channel_${guid} set revision=? where channel_id=?`, [revision, channelId]);
},
setChannelItemDetail: async (guid, channelId, revision, detail) => {
await db.current.executeSql(`UPDATE channel_${guid} set detail_revision=?, detail=? where channel_id=?`, [revision, encodeObject(detail), channelId]);
},
setChannelItemSummary: async (guid, channelId, revision, summary) => {
await db.current.executeSql(`UPDATE channel_${guid} set topic_revision=?, summary=? where channel_id=?`, [revision, encodeObject(summary), channelId]);
},
getChannelItemView: async (guid, channelId) => {
console.log("HERE", channelId);
const values = await getAppValues(db.current, `SELECT revision, detail_revision, topic_revision FROM channel_${guid} WHERE channel_id=?`, [channelId]);
if (!values.length) {
return {};
}
return {
revision: values[0].revision,
detailRevision: values[0].detail_revision,
topicRevision: values[0].topic_revision,
};
},
getChannelItems: async (guid) => {
const values = await getAppValues(db.current, `SELECT channel_id, revision, detail_revision, topic_revision, detail, summary FROM channel_${guid}`, []);
return values.map(channel => ({
channelId: channel.channel_id,
revision: channel.revision,
detailRevision: channel.detail_revision,
topicRevision: channel.topic_revision,
detail: decodeObject(channel.detail),
summary: decodeObject(channel.summary),
}));
},
}
return { state, actions }
@ -105,4 +164,17 @@ async function getAppValue(sql: SQLite.SQLiteDatabase, id: string, unset) {
return unset;
}
async function getAppValues(sql: SQLite.SQLiteDatabase, query: string, params) {
const res = await sql.executeSql(query, params);
if (!hasResult(res)) {
return [];
}
const values = [];
for (let i = 0; i < res[0].rows.length; i++) {
values.push(res[0].rows.item(i));
}
return values;
}