cleanup on channel context

This commit is contained in:
Roland Osborne 2023-01-04 15:27:29 -08:00
parent fbd46b3f3f
commit b1b26b3fe4
5 changed files with 174 additions and 240 deletions

View File

@ -1,6 +1,6 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function addChannelTopic(token, channelId, datatype, message, assets ): string {
export async function addChannelTopic(token, channelId, datatype, message, assets ) {
if (message == null && (assets == null || assets.length === 0)) {
let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}`,

View File

@ -14,216 +14,107 @@ import { setChannelSubject } from 'api/setChannelSubject';
import { setChannelCard } from 'api/setChannelCard';
import { clearChannelCard } from 'api/clearChannelCard';
import { UploadContext } from 'context/UploadContext';
import CryptoJS from 'crypto-js';
import { JSEncrypt } from 'jsencrypt'
export function useChannelContext() {
const [state, setState] = useState({
init: false,
offsync: false,
channels: new Map(),
});
const upload = useContext(UploadContext);
const access = useRef(null);
const revision = useRef(null);
const setRevision = useRef(null);
const curRevision = useRef(null);
const channels = useRef(new Map());
const next = useRef(null);
const syncing = useRef(false);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }))
}
const unsealKey = (seals, sealKey) => {
let unsealedKey;
if (seals?.length) {
seals.forEach(seal => {
if (seal.publicKey === sealKey.public) {
let crypto = new JSEncrypt();
crypto.setPrivateKey(sealKey.private);
unsealedKey = crypto.decrypt(seal.sealedKey);
}
});
}
return unsealedKey;
}
const sync = async () => {
if (!syncing.current && setRevision.current !== curRevision.current) {
syncing.current = true;
const updateChannels = async () => {
let delta = await getChannels(access.current, revision.current);
for (let channel of delta) {
if (channel.data) {
let cur = channels.current.get(channel.id);
if (cur == null) {
cur = { id: channel.id, data: { } }
}
if (cur.data.detailRevision !== channel.data.detailRevision) {
if (channel.data.channelDetail != null) {
cur.data.channelDetail = channel.data.channelDetail;
try {
const token = access.current;
const revision = curRevision.current;
const delta = await getChannels(token, setRevision.current);
for (let channel of delta) {
if (channel.data) {
let cur = channels.current.get(channel.id);
if (cur == null) {
cur = { id: channel.id, data: { } }
}
if (cur.data.detailRevision !== channel.data.detailRevision) {
if (channel.data.channelDetail != null) {
cur.data.channelDetail = channel.data.channelDetail;
}
else {
let detail = await getChannelDetail(token, channel.id);
cur.data.channelDetail = detail;
}
cur.data.unsealedSubject = null;
cur.data.detailRevision = channel.data.detailRevision;
}
if (cur.data.topicRevision !== channel.data.topicRevision) {
if (channel.data.channelSummary != null) {
cur.data.channelSummary = channel.data.channelSummary;
}
else {
let summary = await getChannelSummary(token, channel.id);
cur.data.channelSummary = summary;
}
cur.data.unsealedSummary = null;
cur.data.topicRevision = channel.data.topicRevision;
}
cur.revision = channel.revision;
channels.current.set(channel.id, cur);
}
else {
let detail = await getChannelDetail(access.current, channel.id);
cur.data.channelDetail = detail;
channels.current.delete(channel.id);
}
cur.data.unsealedChannel = null;
cur.data.detailRevision = channel.data.detailRevision;
}
if (cur.data.topicRevision !== channel.data.topicRevision) {
if (channel.data.channelSummary != null) {
cur.data.channelSummary = channel.data.channelSummary;
}
else {
let summary = await getChannelSummary(access.current, channel.id);
cur.data.channelSummary = summary;
}
cur.data.unsealedSummary = null;
cur.data.topicRevision = channel.data.topicRevision;
}
cur.revision = channel.revision;
channels.current.set(channel.id, { ...cur });
setRevision.current = revision;
updateState({ offsync: false, channels: channels.current });
}
else {
channels.current.delete(channel.id);
catch(err) {
console.log(err);
syncing.current = false;
updateState({ offsync: true });
return;
}
}
}
const setChannels = async (rev) => {
let force = false;
if (rev == null) {
force = true;
rev = revision.current;
}
if (next.current == null) {
next.current = rev;
if (force || revision.current !== rev) {
await updateChannels();
updateState({ init: true, channels: channels.current });
revision.current = rev;
}
let r = next.current;
next.current = null;
if (revision.current !== r) {
setChannels(r);
}
}
else {
next.current = rev;
syncing.current = false;
await sync();
}
}
const actions = {
setToken: (token) => {
if (access.current || syncing.current) {
throw new Error("invalid session state");
}
access.current = token;
channels.current = new Map();
curRevision.current = null;
setRevision.current = null;
setState({ offsync: false, channels: new Map() });
},
clearToken: () => {
access.current = null;
channels.current = new Map();
revision.current = null;
setState({ init: false, channels: new Map() });
},
setRevision: async (rev) => {
setChannels(rev);
curRevision.current = rev;
await sync();
},
addBasicChannel: async (cards, subject) => {
return await addChannel(access.current, 'superbasic', cards, { subject });
addChannel: async (type, subject, cards) => {
return await addChannel(access.current, type, cards, subject);
},
addSealedChannel: async (cards, subject, keys) => {
const key = CryptoJS.lib.WordArray.random(256 / 8);
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ subject }), key, { iv: iv });
const subjectEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64)
const subjectIv = iv.toString();
const keyHex = key.toString();
let seals = [];
let crypto = new JSEncrypt();
keys.forEach(publicKey => {
crypto.setPublicKey(publicKey);
const sealedKey = crypto.encrypt(keyHex);
seals.push({ publicKey, sealedKey });
});
const data = { subjectEncrypted, subjectIv, seals };
return await addChannel(access.current, 'sealed', cards, data);
removeChannel: async (channelId) => {
return await removeChannel(access.current, channelId);
},
unsealChannelSubject: (channelId, sealKey) => {
try {
const channel = channels.current.get(channelId);
const { subjectEncrypted, subjectIv, seals } = JSON.parse(channel.data.channelDetail.data);
const unsealedKey = unsealKey(seals, sealKey);
if (unsealKey) {
const iv = CryptoJS.enc.Hex.parse(subjectIv);
const key = CryptoJS.enc.Hex.parse(unsealedKey);
const enc = CryptoJS.enc.Base64.parse(subjectEncrypted);
const cipher = CryptoJS.lib.CipherParams.create({ ciphertext: enc, iv: iv });
const dec = CryptoJS.AES.decrypt(cipher, key, { iv: iv });
channel.data.unsealedChannel = JSON.parse(dec.toString(CryptoJS.enc.Utf8));
channels.current.set(channel.id, { ...channel });
updateState({ channels: channels.current });
}
}
catch(err) {
console.log(err);
}
},
isUnsealed: (channelId, sealKey) => {
try {
const channel = channels.current.get(channelId);
const { seals } = JSON.parse(channel.data.channelDetail.data);
for (let i = 0; i < seals.length; i++) {
if (seals[i].publicKey === sealKey.public) {
return sealKey.private != null;
}
}
}
catch(err) {
console.log(err);
}
return false;
},
unsealChannelSummary: (channelId, sealKey) => {
try {
const channel = channels.current.get(channelId);
const { seals } = JSON.parse(channel.data.channelDetail.data);
const { messageEncrypted, messageIv } = JSON.parse(channel.data.channelSummary.lastTopic.data);
const unsealedKey = unsealKey(seals, sealKey);
if (unsealKey) {
const iv = CryptoJS.enc.Hex.parse(messageIv);
const key = CryptoJS.enc.Hex.parse(unsealedKey);
const enc = CryptoJS.enc.Base64.parse(messageEncrypted);
const cipher = CryptoJS.lib.CipherParams.create({ ciphertext: enc, iv: iv });
const dec = CryptoJS.AES.decrypt(cipher, key, { iv: iv });
channel.data.unsealedSummary = JSON.parse(dec.toString(CryptoJS.enc.Utf8));
channels.current.set(channel.id, { ...channel });
updateState({ channels: channels.current });
}
}
catch(err) {
console.log(err);
}
},
setChannelSubject: async (channelId, subject) => {
return await setChannelSubject(access.current, channelId, 'superbasic', { subject });
},
setChannelSealedSubject: async (channelId, subject, sealKey) => {
const channel = channels.current.get(channelId);
let { seals, subjectEncrypted, subjectIv } = JSON.parse(channel.data.channelDetail.data);
if (seals?.length) {
seals.forEach(seal => {
if (seal.publicKey === sealKey.public) {
let crypto = new JSEncrypt();
crypto.setPrivateKey(sealKey.private);
const unsealedKey = crypto.decrypt(seal.sealedKey);
const key = CryptoJS.enc.Hex.parse(unsealedKey);
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ subject }), key, { iv: iv });
subjectEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64)
subjectIv = iv.toString();
}
});
}
const data = { subjectEncrypted, subjectIv, seals };
return await setChannelSubject(access.current, channelId, 'sealed', data);
setChannelSubject: async (channelId, type, subject) => {
return await setChannelSubject(access.current, channelId, type, subject);
},
setChannelCard: async (channelId, cardId) => {
return await setChannelCard(access.current, channelId, cardId);
@ -231,47 +122,28 @@ export function useChannelContext() {
clearChannelCard: async (channelId, cardId) => {
return await clearChannelCard(access.current, channelId, cardId);
},
removeChannel: async (channelId) => {
return await removeChannel(access.current, channelId);
},
removeChannelTopic: async (channelId, topicId) => {
await removeChannelTopic(access.current, channelId, topicId);
try {
await setChannels(null);
}
catch (err) {
console.log(err);
unsealChannelSubject: async (channelId, unsealed, revision) => {
const channel = channels.current.get(channelId);
if (channel.revision === revision) {
channel.data.unsealedSubject = unsealed;
channels.current.set(channelId, channel);
updateState({ channels: channels.current });
}
},
setChannelTopicSubject: async (channelId, topicId, data) => {
await setChannelTopicSubject(access.current, channelId, topicId, 'superbasictopic', data);
try {
await setChannels(null);
}
catch (err) {
console.log(err);
unsealChannelSummary: async (channelId, unsealed, revision) => {
const channel = channels.current.get(channelId);
if (channel.revision === revision) {
channel.data.unsealedSummary = unsealed;
channels.current.set(channelId, chanel);
updateState({ channels: channels.current });
}
},
setSealedChannelTopicSubject: async (channelId, topicId, data, sealKey) => {
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const key = CryptoJS.enc.Hex.parse(sealKey);
const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ message: data }), key, { iv: iv });
const messageEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64)
const messageIv = iv.toString();
await setChannelTopicSubject(access.current, channelId, topicId, 'sealedtopic', { messageEncrypted, messageIv });
try {
await setChannels(null);
}
catch (err) {
console.log(err);
}
},
addChannelTopic: async (channelId, message, files) => {
addTopic: async (channelId, type, message, files) => {
if (files?.length) {
const topicId = await addChannelTopic(access.current, channelId, null, null, null);
upload.actions.addTopic(access.current, channelId, topicId, files, async (assets) => {
message.assets = assets;
await setChannelTopicSubject(access.current, channelId, topicId, 'superbasictopic', message);
const subject = message(assets);
await setChannelTopicSubject(access.current, channelId, topicId, type, subject);
}, async () => {
try {
await removeChannelTopic(access.current, channelId, topicId);
@ -282,7 +154,8 @@ export function useChannelContext() {
});
}
else {
await addChannelTopic(access.current, channelId, 'superbasictopic', message, files);
const subject = message([]);
await addChannelTopic(access.current, channelId, type, subject);
}
try {
await setChannels(null);
@ -290,21 +163,16 @@ export function useChannelContext() {
catch (err) {
console.log(err);
}
},
addSealedChannelTopic: async (channelId, sealKey, message) => {
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const key = CryptoJS.enc.Hex.parse(sealKey);
const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ message }), key, { iv: iv });
const messageEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64)
const messageIv = iv.toString();
await addChannelTopic(access.current, channelId, 'sealedtopic', { messageEncrypted, messageIv });
removeTopic: async (channelId, topicId) => {
await removeChannelTopic(access.current, channelId, topicId);
},
getChannel: (channelId) => {
return channels.current.get(channelId);
setTopicSubject: async (channelId, topicId, type, subject) => {
await setChannelTopicSubject(access.current, channelId, topicId, type, subject);
},
getChannelRevision: (channelId) => {
let channel = channels.current.get(channelId);
return channel?.revision;
getChannelTopicAssetUrl: (channelId, topicId, assetId) => {
return getChannelTopicAssetUrl(access.current, channelId, topicId, assetId);
},
getChannelTopics: async (channelId, revision, count, begin, end) => {
return await getChannelTopics(access.current, channelId, revision, count, begin, end);
@ -312,10 +180,10 @@ export function useChannelContext() {
getChannelTopic: async (channelId, topicId) => {
return await getChannelTopic(access.current, channelId, topicId);
},
getChannelTopicAssetUrl: (channelId, topicId, assetId) => {
return getChannelTopicAssetUrl(access.current, channelId, topicId, assetId);
}
}
resync: async () => {
await sync();
},
};
return { state, actions }
}

View File

@ -7,6 +7,7 @@ import { getProfileImageUrl } from 'api/getProfileImageUrl';
export function useProfileContext() {
const [state, setState] = useState({
offsync: false,
identity: {},
imageUrl: null,
});
@ -24,36 +25,41 @@ export function useProfileContext() {
syncing.current = true;
try {
const token = access.current;
const revision = curRevision.current;
const identity = await getProfile(access.current);
const imageUrl = identity.image ? getProfileImageUrl(access.current, revision) : null;
updateState({ identity, imageUrl });
const imageUrl = identity.image ? getProfileImageUrl(token, revision) : null;
setRevision.current = revision;
updateState({ offsync: false, identity, imageUrl });
}
catch(err) {
console.log(err);
syncing.current = false;
updateState({ offsync: true });
return;
}
syncing.current = false;
sync();
await sync();
}
}
const actions = {
setToken: (token) => {
if (access.current || syncing.current) {
throw new Error("invalid session state");
}
access.current = token;
curRevision.current = null;
setRevision.current = null;
setState({ offsync: false, identity: {}, imageUrl: null });
},
clearToken: () => {
access.current = null;
curRevision.current = null;
setRevision.current = null;
setState({ identity: {}, imageUrl: null });
},
setRevision: (rev) => {
setRevision: async (rev) => {
curRevision.current = rev;
sync();
await sync();
},
setProfileData: async (name, location, description) => {
await setProfileData(access.current, name, location, description);
@ -64,6 +70,9 @@ export function useProfileContext() {
getHandleStatus: async (name) => {
return await getUsername(name, access.current);
},
resync: async () => {
await sync();
},
}
return { state, actions }

View File

@ -0,0 +1,61 @@
import React, { useState, useEffect, useContext } from 'react';
import {render, act, screen, waitFor, fireEvent} from '@testing-library/react'
import { ChannelContextProvider, ChannelContext } from 'context/ChannelContext';
import * as fetchUtil from 'api/fetchUtil';
let channelContext = null;
function ChannelView() {
const [renderCount, setRenderCount] = useState(0);
const channel = useContext(ChannelContext);
channelContext = channel;
useEffect(() => {
setRenderCount(renderCount + 1);
}, [channel.state]);
return (
<div>
<span data-testid="count">{ renderCount }</span>
</div>
);
}
function ChannelTestApp() {
return (
<ChannelContextProvider>
<ChannelView />
</ChannelContextProvider>
)
}
const realFetchWithTimeout = fetchUtil.fetchWithTimeout;
const realFetchWithCustomTimeout = fetchUtil.fetchWithCustomTimeout;
let fetching = (url, options) => Promise.resolve({ json: () => Promise.resolve([])});
beforeEach(() => {
const mockFetch = jest.fn().mockImplementation((url, options) => fetching(url, options));
fetchUtil.fetchWithTimeout = mockFetch;
fetchUtil.fetchWithCustomTimeout = mockFetch;
});
afterEach(() => {
fetchUtil.fetchWithTimeout = realFetchWithTimeout;
fetchUtil.fetchWithCustomTimeout = realFetchWithCustomTimeout;
});
test('testing channel sync', async () => {
render(<ChannelTestApp />);
await waitFor(async () => {
expect(channelContext).not.toBe(null);
});
await act( async () => {
channelContext.actions.setToken('abc123');
await channelContext.actions.setRevision(1);
});
});

View File

@ -60,7 +60,7 @@ afterEach(() => {
fetchUtil.fetchWithCustomTimeout = realFetchWithCustomTimeout;
});
test('testing', async () => {
test('testing profile sync', async () => {
render(<ProfileTestApp />);
await waitFor(async () => {
@ -107,10 +107,6 @@ test('testing', async () => {
await profileContext.actions.clearToken();
});
await waitFor(async () => {
expect(screen.getByTestId('name').textContent).toBe("");
});
});