umc-managed-store
Version:
extend umc store with structure-ensure and auto-clear to make store safety
449 lines (382 loc) • 15.9 kB
JavaScript
const bytes = require('bytes');
const cache = {};
const STORE_CACHE_KEY = 'fyadmin:all-in-one-store';//注意该名称不能更改!!!!!!!!!!!!!!, 否则会导致用户缓存数据丢失, 要重新登录
const CLEAR_THRESHOLD_SIZE = bytes('10mb') || 10*1024*1024;
const PORTAL_FEED_TYPES = 'feed,activity,work'.split(',');
const PORTAL_MEM_TYPES = 'friend,followed,follower'.split(',');
const PORTAL_TYPES = PORTAL_FEED_TYPES.concat(PORTAL_MEM_TYPES).concat(('team,activityOrder'.split(',')));
const REC_TYPES = 'member,activity,work,team'.split(',');
const PULL_TYPES = 'feed,liked,at,reply'.split(',');
const RL_PERIODS = 'forever,month,day'.split(',');
const RL_MEMBERS = RL_PERIODS.map(key=>'member-'+key);
global.WORK_CATEGORY = ['相当']
const RL_WORKS = RL_PERIODS.map(key=>'work-'+key).concat(global.WORK_CATEGORY.map(cat=>'work-forever-'+cat));
const RL_NAMES = RL_MEMBERS.concat(RL_WORKS);
const SEARCH_FEED_TYPES = 'feed,activity,work,thread'.split(',');
const SEARCH_MEM_TYPES = 'member,friend,followed,follower'.split(',');
const SEARCH_TYPES = SEARCH_FEED_TYPES.concat(SEARCH_MEM_TYPES, 'team');
const STATIC_ROOMS = {
'#at': {id: '#at', type: 'static'},
'#reply': {id: '#reply', type: 'static'},
'#liked': {id: '#liked', type: 'static'},
}
const umc = require('umc');
let now = Date.now();
let initialState = ensureStore({});
console.log('initialState elapsed ', Date.now()-now)
const store = umc(initialState);
const DEFAULT_SET_OPTIONS = {ensure: true, save: true};
const oldSetState = store.setState;
store.setState = function(state, options){
options || (options = {});
options = Object.assign({}, DEFAULT_SET_OPTIONS, options);
//确保数据结构安全
if(options.ensure){
state = Object.assign({}, store.state, state);
state = ensureStore(state);
}
oldSetState.call(store, state);
//save to the cache
if(options.save){
if(saveStore) saveStore(store.state);//should use store.state, for state maybe incomplete
}
}
//更改store的结构后, 请同步在store.readme.md文件中修改
//!!!! 这个方法要尽可能的安全, 防止因为clear/unload等其他因素导致store.state被破坏, 程序直接gg
//!!!! 所谓安全, 就是个个key都要确保存在, 且其值类型正确(如array不能为null等); 不存在的实体数据的引用要及时删除
function ensureStore(state){
if(!state) state = {};
//以下定义中, 如无特殊说明, rows都应该是是id的集合
//当前登陆者
if(!state.auth) state.auth = {isLogin: false, token: '', id: ''};
//行数据集, 基本上上所有的行数据都在这里, 其他地方都是对它们的引用. 结构基本为: {id: row}
if(!state.members) state.members = {};//所有的会员
if(!state.feeds) state.feeds = {};//所有的feed, 包括作品/活动/帖子/评论
if(!state.teams) state.teams = {};//所有的小组
if(!state.activityOrders) state.activityOrders = {};//活动报名的订单信息, {orderId: order}
const members = state.members, feeds = state.feeds, teams = state.teams, activityOrders = state.activityOrders;
//各feed的关系数据. 示例结构如下.
if(!state.feedRelations){
state.feedRelations = {
'-1': {//示例数据.
reply: {rows: []},
hotReply: {rows: []},
liked: {rows: []},
activityOrder: {rows: []},
}
}
}else{
for(let kk in state.feedRelations){
let vv = state.feedRelations[kk];
if(!vv.reply) vv.reply = {rows: []};
if(!vv.reply.rows) vv.reply.rows = [];
vv.reply.rows = vv.reply.rows.filter(rowId=>!!feeds[rowId])
if(!vv.hotReply) vv.hotReply = {rows: []};
if(!vv.hotReply.rows) vv.hotReply.rows = [];
vv.hotReply.rows = vv.hotReply.rows.filter(rowId=>!!feeds[rowId])
if(!vv.liked) vv.liked = {rows: []};
if(!vv.liked.rows) vv.liked.rows = [];
vv.liked.rows = vv.liked.rows.filter(rowId=>!!members[rowId])
if(!vv.activityOrder) vv.activityOrder = {rows: []};
if(!vv.activityOrder.rows) vv.activityOrder.rows = [];
vv.activityOrder.rows = vv.activityOrder.rows.filter(rowId=>!!activityOrders[rowId])
}
}
//team的相关数据,
if(!state.teamRelations){
state.teamRelations = {
'-1': {
thread: {rows: []},
member: {rows: []},
}
}
}else{
for(let kk in state.teamRelations){
let vv = state.teamRelations[kk];
if(!vv.thread) vv.thread = {rows: []};
if(!vv.thread.rows) vv.thread.rows = [];
vv.thread.rows = vv.thread.rows.filter(rowId=>!!feeds[rowId])
if(!vv.member) vv.member = {rows: []};
if(!vv.member.rows) vv.member.rows = [];
vv.member.rows = vv.member.rows.filter(rowId=>!!members[rowId])
}
}
//会员自身额外的一些信息
if(!state.portal) state.portal = {};
const portal = state.portal;
PORTAL_TYPES.forEach(type=>{
if(!portal[type]) portal[type] = {rows: [], count: 0};
if(!portal[type].count) portal[type].count = 0;
if(!portal[type].rows) portal[type].rows = [];
let removed = 0;
let rows = portal[type].rows.filter(rowId=>{
let exists = true;
if(PORTAL_FEED_TYPES.indexOf(type)>=0) exists = !!feeds[rowId];
else if(PORTAL_MEM_TYPES.indexOf(type)>=0) exists = !!members[rowId];
else if(type==='team') exists = !!teams[rowId];
if(!exists) removed++;
return exists;
})
portal[type].rows = rows;
portal[type].count = Math.max(portal[type].count-removed, 0);
})
//推荐内容
if(!state.recommend) state.recommend = {};
const recommend = state.recommend;
REC_TYPES.forEach(type=>{
if(!recommend[type]) recommend[type] = {rows: []};
if(!recommend[type].rows) recommend[type].rows = [];
recommend[type].rows = recommend[type].rows.filter(rowId=>{
if(type==='work' || type==='activity') return !!feeds[rowId];
else if(type==='member') return !!members[rowId];
else if(type==='team') return !!teams[rowId];
return true;
})
})
//定时拉取的消息: 关注/点赞/@/评论
if(!state.pull) state.pull = {};
const pull = state.pull;
PULL_TYPES.forEach(type=>{
if(!pull[type]) pull[type] = {rows: [], newlyCount: 0};
if(!pull[type].newlyCount) pull[type].newlyCount = 0;
if(!pull[type].rows) pull[type].rows = [];
pull[type].rows = pull[type].rows.filter(rowId=>!!feeds[rowId]);
})
//排行榜
if(!state.ranklist) state.ranklist = {};
const ranklist = state.ranklist;
RL_NAMES.forEach(name=>{
if(!ranklist[name]) ranklist[name] = {rows: [], };
if(!ranklist[name].rows) ranklist[name].rows = [];
ranklist[name].rows = ranklist[name].rows.filter(rowId=>{
if(RL_MEMBERS.indexOf(name)>=0) return !!members[rowId];
else if(RL_WORKS.indexOf(name)>=0) return !!feeds[rowId];
return true;
})
})
//搜索结果
if(!state.search) state.search = {};
const search = state.search;
SEARCH_TYPES.forEach(type=>{
if(!search[type]) search[type] = {rows: [], term: ''};
if(!search[type].rows) search[type].rows = [];
search[type].rows = search[type].rows.filter(rowId=>{
if(SEARCH_FEED_TYPES.indexOf(type)>=0) return !!feeds[rowId];
else if(SEARCH_MEM_TYPES.indexOf(type)>=0) return !!members[rowId];
else if(type==='team') return !!teams[rowId];
return true;
})
})
//所有的房间, exchange里用的, room: {id: string, type: enum(static|team|member|group),
// desc: string, updatedAt: timestamp, unreadCount: integer}
if(!state.rooms){
state.rooms = STATIC_ROOMS;
}
if(!state.chats) state.chats = {};//所有含聊天房间的会话信息, 结构: {roomId: [messages]}. 消息都存储在sqlite里
if(!state.exchange){
state.exchange = {
rooms: Object.keys(STATIC_ROOMS),
unreadCount: 0, //totally unread message count for all rooms
open: '',//打开的room id
}
}
//自动更新
if(!state.update){
state.update = {
version: '',//当前native code的版本
local: {//本地包
isFirstRun: false,//当前更新是否第一次运行
description: '',//当前更新的描述
binaryVersion: '',//app安装时的版本号
labelVersion: '',//codepush update label
lastUpdateSize: '',//上次下载的更新包大小
},
remote: {//远程更新包
status: '',//found/ignored/downloading/background-downloading/downloaded/restart-later
description: '',
binaryVersion: '',
labelVersion: '',
progress: 0,//下载进度, 0-100
size: '',//更新包总大小
},
}
}
if(!state.ui) state.ui = {selectedMainBar: 'home'};
return state;
}
function loadFromCache(){
// return cache.get(STORE_CACHE_KEY).then(state=>{
// if(!state) return;
// if(typeof state !== 'object') return;
// //初始化部分数据
// const {auth, ui, exchange, rooms} = state;
// if(auth) auth.isLogin = false;
// if(ui) ui.selectedMainBar = 'home';
// //修正exchange的数据
// if(exchange){
// let unreadCount = 0;
// if(rooms && Array.isArray(exchange.rooms)){
// exchange.rooms.forEach(roomId=>{
// let room = rooms[roomId];
// if(!room) return;
// unreadCount += (room.unreadCount || 0);
// })
// }
// exchange.unreadCount = unreadCount;
// exchange.open = '';
// }
// store.setState(state);
// })
}
store.load = loadFromCache;
let lastAutoClear = 0;
const AUTO_CLEAR_INTERVAL = 5*60*1000;//5mins
function saveStore(state){
if(!state) return;
let saved = {};
for(let kk in state){
if(kk==='chats') continue;
saved[kk] = state[kk];
}
return cache.set(STORE_CACHE_KEY, saved, {returnSize: true}).then(result=>{
if(!result) return;
state.size = bytes(result.size);//这里直接设置size没有问题(不需要setState, 影响性能)
// console.debug('size', state.size);
if(result.size>=CLEAR_THRESHOLD_SIZE){
let now = Date.now();
if(now-lastAutoClear<AUTO_CLEAR_INTERVAL) return;
lastAutoClear = now;
return store.clear();
}
})
}
//一般是登出时调用
//清除登陆者特有的数据, 一般只要清除引用数据即可, 不需要清除实体数据(实体数据可以通过clearStore来清理)
function unloadStore(){
// const newState = {...store.state};
// const {pull, portal, teams, members, feeds, exchange, ui, feedRelations} = newState;
// ui.selectedMainBar = 'home';
// PULL_TYPES.forEach(key=>{
// pull[key] = {rows: [], newlyCount: 0}
// })
// PORTAL_TYPES.forEach(key=>{
// portal[key] = {rows: [], count: 0, }
// })
// exchange.unreadCount = 0;
// newState.rooms = {...STATIC_ROOMS};
// exchange.rooms = Object.keys(STATIC_ROOMS);
// exchange.open = '';
// for(let memberId in members){
// members[memberId] = {...members[memberId], followed: false};
// }
// for(let teamId in teams){
// teams[teamId] = {...teams[teamId], joined: false};
// }
// for(let feedId in feeds){
// feeds[feedId] = {...feeds[feedId], liked: false, activityOrderStatus: ''};
// }
// for(let feedId in feedRelations){
// feedRelations[feedId].activityOrder = {rows: []};//非登录用户既看不到自己参与的活动, 也看不到活动的报名情况
// }
// newState.activityOrders = {};//非登录用户既看不到自己参与的活动, 也看不到活动的报名情况
// store.setState(newState);
}
store.unload = unloadStore;
//清理store, 将无用的数据移除
//options.truncate: 对引用进行截断, 取前多少行id, 值为number|true. 默认行数为60.
// 这个参数是为了避免循环引用导致无法释放(如reply与feed, activity与order之间)
function clearStore(options){
options || (options = {});
if(options.truncate===true) options.truncate = 60;
options.truncate = ~~(+options.truncate);
if(options.truncate<=0) options.truncate = undefined;
const newState = Object.assign({}, store.state);
const feeds = newState.feeds, teams = newState.teams, members = newState.members, feedRelations = newState.feedRelations,
teamRelations = newState.teamRelations, activityOrders = newState.activityOrders, rooms = newState.rooms,
auth = newState.auth, portal = newState.portal, pull = newState.pull, recommend = newState.recommend,
search = newState.search, ranklist = newState.ranklist;
//先计算各个实体的引用情况
const feedHasRef = {}, memberHasRef = {}, teamHasRef = {}, activityOrderHasRef = {};
if(auth.id) memberHasRef[auth.id] = true;
for(let feedId in feeds){
let feed = feeds[feedId];
if(feed.feedId) feedHasRef[feed.feedId] = true;
if(feed.memberId) memberHasRef[feed.memberId] = true;
}
for(let kk in activityOrders){
let order = activityOrders[kk];
if(order.activityId) feedHasRef[order.activityId] = true;
if(order.memberId) memberHasRef[order.memberId] = true;
}
for(let kk in feedRelations){
let vv = feedRelations[kk];
vv.reply.rows.slice(0, options.truncate).forEach(rowId=>(feedHasRef[rowId] = true));
vv.hotReply.rows.slice(0, options.truncate).forEach(rowId=>(feedHasRef[rowId] = true))
vv.liked.rows.slice(0, options.truncate).forEach(rowId=>(memberHasRef[rowId] = true))
vv.activityOrder.rows.slice(0, options.truncate).forEach(rowId=>(activityOrderHasRef[rowId] = true))
}
for(let kk in teamRelations){
let vv = teamRelations[kk];
vv.thread.rows.slice(0, options.truncate).forEach(rowId=>(feedHasRef[rowId] = true));
vv.member.rows.slice(0, options.truncate).forEach(rowId=>(memberHasRef[rowId] = true));
}
PORTAL_TYPES.forEach(type=>{
portal[type].rows.slice(0, options.truncate).forEach(rowId=>{
if(PORTAL_FEED_TYPES.indexOf(type)>=0) feedHasRef[rowId] = true;
else if(PORTAL_MEM_TYPES.indexOf(type)>=0) memberHasRef[rowId] = true;
else if(type==='team') teamHasRef[rowId] = true;
else if(type==='activityOrder') activityOrderHasRef[rowId] = true;
})
})
REC_TYPES.forEach(type=>{
recommend[type].rows.slice(0, options.truncate).forEach(rowId=>{
if(type==='work' || type==='activity') feedHasRef[rowId] = true;
else if(type==='member') memberHasRef[rowId] = true;
else if(type==='team') teamHasRef[rowId] = true;
})
})
PULL_TYPES.forEach(type=>{
pull[type].rows.slice(0, options.truncate).forEach(rowId=>(feedHasRef[rowId] = true));
})
RL_NAMES.forEach(name=>{
ranklist[name].rows.slice(0, options.truncate).forEach(rowId=>{
if(RL_MEMBERS.indexOf(name)>=0) memberHasRef[rowId] = true;
else if(RL_WORKS.indexOf(name)>=0) feedHasRef[rowId] = true;
})
})
SEARCH_TYPES.forEach(type=>{
search[type].rows.slice(0, options.truncate).forEach(rowId=>{
if(SEARCH_FEED_TYPES.indexOf(type)>=0) feedHasRef[rowId] = true;
else if(SEARCH_MEM_TYPES.indexOf(type)>=0) memberHasRef[rowId] = true;
else if(type==='team') teamHasRef[rowId] = true;
})
})
for(let kk in rooms){
let room = rooms[kk];
if(room.type==='member') memberHasRef[room.id] = true;
else if(room.type==='team') teamHasRef[room.id] = true;
}
//清理各实体数据及其对应的关系数据
Object.keys(feeds).forEach(feedId=>{
if(!feedHasRef[feedId]){
delete feeds[feedId];
delete feedRelations[feedId];
}
})
Object.keys(members).forEach(memberId=>{
if(!memberHasRef[memberId]) delete members[memberId];
})
Object.keys(teams).forEach(teamId=>{
if(!teamHasRef[teamId]){
delete teams[teamId];
delete teamRelations[teamId];
}
})
Object.keys(activityOrders).forEach(orderId=>{
if(!activityOrderHasRef[orderId]) delete activityOrders[orderId];
})
store.setState(newState);
}
store.clear = clearStore;
module.exports = store;