oak-frontend-base/lib/features/cache.js

459 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cache = void 0;
const Feature_1 = require("../types/Feature");
const lodash_1 = require("oak-domain/lib/utils/lodash");
const CacheStore_1 = require("../cacheStore/CacheStore");
const Exception_1 = require("oak-domain/lib/types/Exception");
const assert_1 = require("oak-domain/lib/utils/assert");
const constant_1 = require("../constant/constant");
const filter_1 = require("oak-domain/lib/store/filter");
const DEFAULT_KEEP_FRESH_PERIOD = 600 * 1000; // 10分钟不刷新
;
class Cache extends Feature_1.Feature {
cacheStore;
aspectWrapper;
syncEventsCallbacks;
contextBuilder;
refreshing = 0;
savedEntities;
keepFreshPeriod;
localStorage;
getFullDataFn;
refreshRecords = {};
context;
constructor(storageSchema, aspectWrapper, frontendContextBuilder, checkers, getFullData, localStorage, savedEntities, keepFreshPeriod) {
super();
this.aspectWrapper = aspectWrapper;
this.syncEventsCallbacks = [];
this.cacheStore = new CacheStore_1.CacheStore(storageSchema);
this.contextBuilder = () => frontendContextBuilder()(this.cacheStore);
this.savedEntities = ['actionAuth', 'i18n', 'path', ...(savedEntities || [])];
this.keepFreshPeriod = keepFreshPeriod || DEFAULT_KEEP_FRESH_PERIOD;
this.localStorage = localStorage;
checkers.forEach((checker) => this.cacheStore.registerChecker(checker));
this.getFullDataFn = getFullData;
// 现在这个init变成了异步行为不知道有没有影响。by Xc 20231126
this.initSavedLogic();
}
/**
* 处理cache中需要缓存的数据
*/
async initSavedLogic() {
const data = {};
await Promise.all(this.savedEntities.map(async (entity) => {
// 加载缓存的数据项
const key = `${constant_1.LOCAL_STORAGE_KEYS.cacheSaved}:${entity}`;
const cached = await this.localStorage.load(key);
if (cached) {
data[entity] = cached;
}
// 加载缓存的时间戳项
const key2 = `${constant_1.LOCAL_STORAGE_KEYS.cacheRefreshRecord}:${entity}`;
const cachedTs = await this.localStorage.load(key2);
if (cachedTs) {
this.refreshRecords[entity] = cachedTs;
}
}));
this.cacheStore.resetInitialData(data);
this.cacheStore.onCommit(async (result) => {
const entities = Object.keys(result);
const referenced = (0, lodash_1.intersection)(entities, this.savedEntities);
if (referenced.length > 0) {
const saved = this.cacheStore.getCurrentData(referenced);
for (const entity in saved) {
const key = `${constant_1.LOCAL_STORAGE_KEYS.cacheSaved}:${entity}`;
await this.localStorage.save(key, saved[entity]);
}
}
});
}
getSchema() {
return this.cacheStore.getSchema();
}
/* getCurrentUserId(allowUnloggedIn?: boolean) {
const context = this.contextBuilder && this.contextBuilder();
return context?.getCurrentUserId(allowUnloggedIn);
} */
async exec(name, params, callback, dontPublish) {
try {
this.refreshing++;
const { result, opRecords, message } = await this.aspectWrapper.exec(name, params);
if (opRecords) {
this.syncInner(opRecords);
}
this.refreshing--;
callback && callback(result, opRecords);
if (opRecords && opRecords.length > 0 && !dontPublish) {
this.publish();
}
return {
result,
message,
};
}
catch (e) {
// 如果是数据不一致错误,这里可以让用户知道
this.refreshing--;
if (e instanceof Exception_1.OakException) {
const { opRecord } = e;
if (opRecord) {
this.syncInner([opRecord]);
this.publish();
}
}
throw e;
}
}
saveRefreshRecord(entity) {
const records = this.refreshRecords[entity];
(0, assert_1.assert)(records);
const key2 = `${constant_1.LOCAL_STORAGE_KEYS.cacheRefreshRecord}:${entity}`;
this.localStorage.save(key2, records);
}
addRefreshRecord(entity, key, now) {
const originTimestamp = this.refreshRecords[entity] && this.refreshRecords[entity][key];
if (this.refreshRecords[entity]) {
Object.assign(this.refreshRecords[entity], {
[key]: now,
});
}
else {
Object.assign(this.refreshRecords, {
[entity]: {
[key]: now,
}
});
}
if (originTimestamp) {
return () => this.addRefreshRecord(entity, key, originTimestamp);
}
return () => undefined;
}
async refresh(entity, selection, option, callback, refreshOption) {
// todo 还要判定没有aggregation
const { dontPublish, useLocalCache } = refreshOption || {};
const onlyReturnFresh = refreshOption?.useLocalCache?.onlyReturnFresh;
let undoFns = [];
const originFilter = selection.filter;
if (useLocalCache) {
(0, assert_1.assert)(!selection.indexFrom && !selection.count, '用cache的查询不能使用分页');
(0, assert_1.assert)(this.savedEntities.includes(entity), `${entity}不在系统设置的应缓存对象当中`);
const { keys, gap } = useLocalCache;
let oldest = Number.MAX_SAFE_INTEGER;
keys.forEach((k) => {
const last = this.refreshRecords[entity] && this.refreshRecords[entity][k];
if (typeof last === 'number') {
if (last < oldest) {
oldest = last;
}
}
else {
// 说明这个key没有取过直接赋0
oldest = 0;
}
});
const gap2 = gap || this.keepFreshPeriod;
const now = Date.now();
if (oldest < Number.MAX_SAFE_INTEGER && oldest > now - gap2) {
// 说明可以用localCache的数据不用去请求
if (process.env.NODE_ENV === 'development') {
// console.warn('根据keepFresh规则省略了一次请求数据的行为', entity, selection);
}
if (onlyReturnFresh) {
return {
data: [],
};
}
const data = this.get(entity, selection);
return {
data,
};
}
else {
if (oldest > 0) {
// 说明key曾经都取过了只取updateAt在oldest之后的数据
selection.filter = (0, filter_1.combineFilters)(entity, this.getSchema(), [selection.filter, {
$$updateAt$$: {
$gte: oldest,
}
}]);
}
undoFns = keys.map((k) => this.addRefreshRecord(entity, k, now));
}
}
try {
const { result: { data: sr, total } } = await this.exec('select', {
entity,
selection,
option,
}, callback, dontPublish);
let filter2 = {
id: {
$in: Object.keys(sr),
}
};
if (undoFns.length > 0 && !onlyReturnFresh) {
filter2 = originFilter;
}
const selection2 = Object.assign({}, selection, {
filter: filter2,
});
const data = this.get(entity, selection2, undefined, sr);
if (useLocalCache) {
this.saveRefreshRecord(entity);
}
return {
data,
total,
};
}
catch (err) {
undoFns && undoFns.forEach((fn) => fn());
throw err;
}
}
async aggregate(entity, aggregation, option) {
const { result } = await this.aspectWrapper.exec('aggregate', {
entity,
aggregation,
option,
});
return result;
}
async operate(entity, operation, option, callback) {
const result = await this.exec('operate', {
entity,
operation,
option,
}, callback);
return result;
}
async count(entity, selection, option, callback) {
const { result } = await this.exec('count', {
entity,
selection,
option,
}, callback);
return result;
}
syncInner(records) {
// sync会异步并发的调用不能用this.context;
const context = this.contextBuilder();
this.cacheStore.sync(records, context);
// 唤起同步注册的回调
this.syncEventsCallbacks.map((ele) => ele(records));
}
sync(records) {
this.syncInner(records);
this.publish();
}
/**
* 前端缓存做operation只可能是测试权限必然回滚
* @param entity
* @param operation
* @returns
*/
tryRedoOperations(operations) {
this.begin();
try {
for (const oper of operations) {
const { entity, operation } = oper;
this.context.operate(entity, operation, {
dontCollect: true,
dontCreateOper: true,
dontCreateModi: true,
});
}
this.rollback();
return true;
}
catch (err) {
this.rollback();
if (!(err instanceof Exception_1.OakUserException)) {
throw err;
}
return err;
}
}
checkOperation(entity, action, data, filter, checkerTypes) {
let autoCommit = false;
if (!this.context) {
this.begin();
autoCommit = true;
}
const operation = {
action,
filter,
data
};
try {
this.cacheStore.check(entity, operation, this.context, checkerTypes);
if (autoCommit) {
this.rollback();
}
return true;
}
catch (err) {
if (autoCommit) {
this.rollback();
}
if (!(err instanceof Exception_1.OakUserException)) {
throw err;
}
return false;
}
}
redoOperation(opers) {
(0, assert_1.assert)(this.context);
opers.forEach((oper) => {
const { entity, operation } = oper;
this.cacheStore.operate(entity, operation, this.context, {
checkerTypes: ['logical'],
dontCollect: true,
dontCreateOper: true,
dontCreateModi: true,
});
});
return;
}
fetchRows(missedRows) {
if (!this.refreshing) {
if (process.env.NODE_ENV === 'development') {
console.warn('缓存被动去获取数据,请查看页面行为并加以优化', missedRows);
}
this.exec('fetchRows', missedRows, async (result, opRecords) => {
// missedRows理论上一定要取到不能为空集。否则就是程序员有遗漏
for (const record of opRecords) {
const { d } = record;
(0, assert_1.assert)(Object.keys(d).length > 0, '在通过fetchRow取不一致数据时返回了空数据请拿该程序员祭天。');
for (const mr of missedRows) {
(0, assert_1.assert)(Object.keys(d[mr.entity]).length > 0, `在通过fetchRow取不一致数据时返回了空数据请拿该程序员祭天。entity是${mr.entity}`);
}
}
});
}
}
getInner(entity, selection, allowMiss) {
let autoCommit = false;
if (!this.context) {
this.begin();
autoCommit = true;
}
try {
const result = this.cacheStore.select(entity, selection, this.context, {
dontCollect: true,
includedDeleted: true,
ignoreAttrMiss: allowMiss || undefined,
});
if (autoCommit) {
this.commit();
}
return result;
}
catch (err) {
if (autoCommit) {
this.rollback();
}
if (err instanceof Exception_1.OakRowUnexistedException) {
if (!allowMiss) {
this.fetchRows(err.getRows());
}
return [];
}
else {
throw err;
}
}
}
/**
* 把select的结果merge到sr中因为select有可能存在aggr数据在这里必须要使用合并后的结果
* sr的数据结构不好规范化描述参见common-aspect中的select接口
* @param entity
* @param rows
* @param sr
*/
mergeSelectResult(entity, rows, sr) {
const mergeSingleRow = (e, r, sr2) => {
for (const k in sr2) {
if (k.endsWith('$$aggr')) {
Object.assign(r, {
[k]: sr2[k],
});
}
else if (r[k]) {
const rel = this.judgeRelation(e, k);
if (rel === 2) {
mergeSingleRow(k, r[k], sr2[k]);
}
else if (typeof rel === 'string') {
mergeSingleRow(rel, r[k], sr2[k]);
}
else {
(0, assert_1.assert)(rel instanceof Array);
(0, assert_1.assert)(r[k] instanceof Array);
const { data } = sr2[k];
this.mergeSelectResult(rel[0], r[k], data);
}
}
}
};
rows.forEach((row) => {
const { id } = row;
if (sr[id]) {
mergeSingleRow(entity, row, sr[id]);
}
});
}
get(entity, selection, allowMiss, sr) {
const rows = this.getInner(entity, selection, allowMiss);
if (sr) {
this.mergeSelectResult(entity, rows, sr);
}
return rows;
}
getById(entity, projection, id, allowMiss) {
return this.getInner(entity, {
data: projection,
filter: {
id,
},
}, allowMiss);
}
judgeRelation(entity, attr) {
return this.cacheStore.judgeRelation(entity, attr);
}
bindOnSync(callback) {
this.syncEventsCallbacks.push(callback);
}
unbindOnSync(callback) {
(0, lodash_1.pull)(this.syncEventsCallbacks, callback);
}
getCachedData() {
return this.cacheStore.getCurrentData();
}
getFullData() {
return this.getFullDataFn();
}
begin() {
(0, assert_1.assert)(!this.context);
this.context = this.contextBuilder();
this.context.begin();
return this.context;
}
commit() {
(0, assert_1.assert)(this.context);
this.context.commit();
this.context = undefined;
}
rollback() {
(0, assert_1.assert)(this.context);
this.context.rollback();
this.context = undefined;
}
buildContext() {
return this.contextBuilder();
}
}
exports.Cache = Cache;