refactor: app store and cache

This commit is contained in:
zerosoul
2022-03-18 16:30:38 +08:00
parent ad994f1eaf
commit 7bb8e9a914
26 changed files with 280 additions and 908 deletions
+27 -1
View File
@@ -2,7 +2,11 @@ import { createApi } from "@reduxjs/toolkit/query/react";
// import toast from "react-hot-toast";
import baseQuery from "./base.query";
import { ContentTypes } from "../config";
import { updateChannel } from "../slices/channels";
import { updateChannel, removeChannel } from "../slices/channels";
import { removeMessage } from "../slices/message";
import { removeChannelSession } from "../slices/message.channel";
import { removeReactionMessage } from "../slices/message.reaction";
import { toggleChannelSetting } from "../slices/ui";
import { onMessageSendStarted } from "./handlers";
export const channelApi = createApi({
reducerPath: "channelApi",
@@ -47,6 +51,28 @@ export const channelApi = createApi({
url: `group/${id}`,
method: "DELETE",
}),
async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
const {
ui: { channelSetting },
channelMessage,
} = getState();
try {
await queryFulfilled;
dispatch(removeChannel(id));
if (id == channelSetting) {
dispatch(toggleChannelSetting());
}
// 删掉该channel下的所有消息&reaction
const mids = channelMessage[id];
if (mids) {
dispatch(removeChannelSession(id));
dispatch(removeMessage(mids));
dispatch(removeReactionMessage(mids));
}
} catch {
console.log("remove channel error");
}
},
}),
sendChannelMsg: builder.mutation({
query: ({ id, content, type = "text" }) => ({
+25
View File
@@ -1,6 +1,9 @@
import { createApi } from "@reduxjs/toolkit/query/react";
// import toast from "react-hot-toast";
import { KEY_UID } from "../config";
import baseQuery from "./base.query";
import { resetAuthData, setUid } from "../slices/auth.data";
import { fullfillContacts } from "../slices/contacts";
import BASE_URL, { ContentTypes } from "../config";
import { onMessageSendStarted } from "./handlers";
export const contactApi = createApi({
@@ -19,6 +22,28 @@ export const contactApi = createApi({
return user;
});
},
async onQueryStarted(data, { dispatch, queryFulfilled }) {
const local_uid = localStorage.getItem(KEY_UID);
try {
const { data: contacts } = await queryFulfilled;
const matchedUser = contacts.find((c) => c.uid == local_uid);
console.log("wtf", contacts, matchedUser);
if (!matchedUser) {
// 用户已注销或被禁用
console.log("no matched user, redirect to login");
dispatch(resetAuthData());
// navigate("/login");
} else {
const markedContacts = contacts.map((u) => {
return u.uid == matchedUser.uid ? { ...u, online: true } : u;
});
dispatch(setUid(matchedUser.uid));
dispatch(fullfillContacts(markedContacts));
}
} catch {
console.log("get contact list error");
}
},
}),
deleteContact: builder.query({
query: (uid) => ({ url: `/admin/user/${uid}`, method: "DELETE" }),
+43 -10
View File
@@ -1,8 +1,8 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import BASE_URL from "../config";
import { updateInviteLink } from "../slices/server";
import { updateInviteLink, updateInfo } from "../slices/server";
import baseQuery from "./base.query";
const defaultExpireDuration = 7 * 24 * 60 * 60;
export const serverApi = createApi({
reducerPath: "serverApi",
baseQuery,
@@ -10,9 +10,17 @@ export const serverApi = createApi({
getServer: builder.query({
query: () => ({ url: `admin/system/organization` }),
transformResponse: (data) => {
data.logo = `${BASE_URL}/resource/organization/logo`;
data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`;
return data;
},
async onQueryStarted(data, { dispatch, queryFulfilled }) {
try {
const { data: server } = await queryFulfilled;
dispatch(updateInfo(server));
} catch {
console.log("get server info error");
}
},
}),
getMetrics: builder.query({
query: () => ({ url: `/admin/system/metrics` }),
@@ -56,9 +64,21 @@ export const serverApi = createApi({
method: "POST",
body: data,
}),
async onQueryStarted(data, { dispatch, queryFulfilled }) {
try {
await queryFulfilled;
dispatch(
updateInfo({
logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`,
})
);
} catch {
console.log("update server logo error");
}
},
}),
createInviteLink: builder.query({
query: (expired_in = 7 * 24 * 60 * 60) => ({
query: (expired_in = defaultExpireDuration) => ({
headers: {
"content-type": "text/plain",
accept: "text/plain",
@@ -66,17 +86,21 @@ export const serverApi = createApi({
url: `/admin/user/create_invite_link?expired_in=${expired_in}`,
responseHandler: (response) => response.text(),
}),
async onQueryStarted(expire, { dispatch, queryFulfilled, getState }) {
const {
expire: prevExp,
link: prevLink,
} = getState().server.inviteLink;
transformResponse: (link) => {
// 替换掉域名
const invite = new URL(link);
return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`;
},
async onQueryStarted(
expire = defaultExpireDuration,
{ dispatch, queryFulfilled }
) {
try {
const { data: link } = await queryFulfilled;
console.log("link", link);
dispatch(updateInviteLink({ expire, link }));
} catch {
dispatch(updateInviteLink({ expire: prevExp, link: prevLink }));
console.log("invite link error");
}
},
}),
@@ -86,6 +110,15 @@ export const serverApi = createApi({
method: "POST",
body: data,
}),
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
const { name: prevName, description: prevDesc } = getState().server;
dispatch(updateInfo(data));
try {
await queryFulfilled;
} catch {
dispatch(updateInfo({ name: prevName, description: prevDesc }));
}
},
}),
}),
});
-131
View File
@@ -1,131 +0,0 @@
import { batch } from "react-redux";
import { addChannelMsg, removeChannelMsg } from "../../slices/message.channel";
import { addMessage, removeMessage, updateMessage } from "../../slices/message";
import { toggleReactionMessage } from "../../slices/message.reaction";
import { addUserMsg, removeUserMsg } from "../../slices/message.user";
import { updateAfterMid } from "../../slices/footprint";
const handler = (data, dispatch, currState) => {
const {
mid,
from_uid,
created_at,
target,
detail: {
mid: detailMid,
content,
content_type,
type,
properties,
expires_in,
detail: innerDetail,
},
} = data;
const common = {
from_uid,
created_at,
content,
content_type,
properties,
expires_in,
};
switch (type) {
case "normal":
case "reply":
// 更新after_mid
dispatch(updateAfterMid(mid));
break;
}
const { ready, loginUid, readUsers = {}, readChannels = {} } = currState;
const to = typeof target.gid !== "undefined" ? "channel" : "user";
const appendMessage = to == "user" ? addUserMsg : addChannelMsg;
const self = from_uid == loginUid;
// 此处有点绕
const id = to == "user" ? (self ? target.uid : from_uid) : target.gid;
const readIndex = (to == "user" ? readUsers[id] : readChannels[id]) || 0;
const read = self ? true : mid < readIndex ? true : false;
switch (type) {
case "normal":
{
batch(() => {
dispatch(
addMessage({
mid,
// 如果是自己发的消息,就是已读
read,
...common,
})
);
// 未推送完 or 不是自己发的消息
console.log("curr state", ready, loginUid, common.from_uid);
if (!ready || loginUid != common.from_uid) {
dispatch(appendMessage({ id, mid }));
}
});
}
break;
case "reply":
{
batch(() => {
dispatch(
addMessage({
mid,
reply_mid: detailMid,
// 如果是自己发的消息,就是已读
read,
...common,
})
);
// 未推送完 or 不是自己发的消息
if (!ready || loginUid != common.from_uid) {
dispatch(appendMessage({ id, mid }));
}
});
}
break;
case "reaction":
{
const removeContextMessage =
to == "user" ? removeUserMsg : removeChannelMsg;
const { type, action, content, content_type, properties } = innerDetail;
switch (type) {
case "like":
{
dispatch(
toggleReactionMessage({ from_uid, mid: detailMid, action })
);
}
break;
case "delete":
{
batch(() => {
dispatch(removeContextMessage({ id, mid: detailMid }));
dispatch(removeMessage(detailMid));
});
}
break;
case "edit":
{
dispatch(
updateMessage({
mid: detailMid,
content,
content_type,
properties,
edited: true,
})
);
}
break;
default:
break;
}
}
break;
default:
break;
}
};
export default handler;
-196
View File
@@ -1,196 +0,0 @@
// for SSE
import { createApi } from "@reduxjs/toolkit/query/react";
import toast from "react-hot-toast";
import BASE_URL from "../../config";
import { setReady } from "../../slices/ui";
import {
fullfillChannels,
addChannel,
removeChannel,
} from "../../slices/channels";
import {
updateUsersVersion,
updateReadChannels,
updateReadUsers,
} from "../../slices/footprint";
import { updateUsersByLogs, updateUsersStatus } from "../../slices/contacts";
import { resetAuthData } from "../../slices/auth.data";
import baseQuery from "../base.query";
import chatMessageHandler from "./chat.handler";
const getQueryString = (params = {}) => {
const sp = new URLSearchParams();
Object.entries(params).forEach(([key, val]) => {
if (val) {
sp.append(key, val);
}
});
return sp.toString();
};
export const streamingApi = createApi({
reducerPath: "streamingApi",
baseQuery,
refetchOnMountOrArgChange: true,
tagTypes: ["streaming"],
endpoints: (builder) => ({
initStreaming: builder.query({
query: () => ({ url: `user/me?ts=${new Date().getTime()}` }),
async onCacheEntryAdded(
arg,
{ dispatch, getState, cacheDataLoaded, cacheEntryRemoved }
) {
// todo
const {
authData: { token },
footprint: { usersVersion, afterMid },
} = getState();
// create a websocket connection when the cache subscription starts
const streaming = new EventSource(
`${BASE_URL}/user/events?${getQueryString({
"api-key": token,
users_version: usersVersion,
after_mid: afterMid,
})}`
);
console.log("cache loaded 1");
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded;
console.log("cache loaded 2");
streaming.onmessage = (evt) => {
const {
ui: { ready },
authData: { uid: loginUid },
footprint: { readUsers, readChannels },
} = getState();
const data = JSON.parse(evt.data);
const { type } = data;
switch (type) {
case "heartbeat":
console.log("heartbeat");
break;
case "ready":
console.log("streaming ready");
dispatch(setReady());
break;
case "users_snapshot":
{
console.log("users snapshot");
const { version } = data;
dispatch(updateUsersVersion(version));
}
break;
case "users_log":
{
console.log("users change logs");
const { logs } = data;
dispatch(updateUsersByLogs(logs));
}
break;
case "user_settings":
case "user_settings_changed":
{
console.log("users settings");
const {
read_index_users = [],
read_index_groups = [],
} = data;
dispatch(updateReadChannels(read_index_groups));
dispatch(updateReadUsers(read_index_users));
}
break;
case "users_state":
case "users_state_changed":
{
let { type, ...rest } = data;
const onlines =
type == "users_state_changed" ? [rest] : rest.users;
dispatch(updateUsersStatus(onlines));
}
break;
case "kick":
{
console.log("kicked");
switch (data.reason) {
case "login_from_other_device":
dispatch(resetAuthData());
toast("kicked from the other device");
break;
case "delete_user":
dispatch(resetAuthData());
toast("sorry, your account has been deleted");
break;
default:
break;
}
}
break;
case "related_groups":
console.log("fullfill channels from streaming", data);
dispatch(fullfillChannels(data.groups));
break;
case "joined_group":
console.log("joined group", data.group);
dispatch(addChannel(data.group));
break;
case "kick_from_group":
console.log("kicked from group", data.gid);
dispatch(removeChannel(data.gid));
break;
case "chat":
{
chatMessageHandler(data, dispatch, {
ready,
loginUid,
readUsers,
readChannels,
});
}
break;
default:
console.log("sse event data", data);
break;
}
};
streaming.onopen = (wtf) => {
console.info("sse opened", wtf);
};
streaming.onerror = (err) => {
switch (err.eventPhase) {
case EventSource.CLOSED:
console.log("sse error closed error");
break;
case EventSource.CONNECTING:
console.log("sse error connecting error");
// streamingApi.util.resetApiState({ type: "initStreaming" });
streamingApi.util.invalidateTags("streaming");
streamingApi.util.updateQueryData("initStreaming");
break;
default:
streaming.close();
console.error("sse error error", err);
// renewToken({ token, refreshToken });
break;
}
};
} catch {
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await cacheEntryRemoved;
// perform cleanup steps once the `cacheEntryRemoved` promise resolves
console.log("close streaming");
streaming.close();
},
}),
}),
});
export const {
useInitStreamingQuery,
useLazyInitStreamingQuery,
} = streamingApi;