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

652 lines
28 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;
var tslib_1 = require("tslib");
var Feature_1 = require("../types/Feature");
var lodash_1 = require("oak-domain/lib/utils/lodash");
var CacheStore_1 = require("../cacheStore/CacheStore");
var Exception_1 = require("oak-domain/lib/types/Exception");
var assert_1 = tslib_1.__importDefault(require("assert"));
var constant_1 = require("../constant/constant");
var filter_1 = require("oak-domain/lib/store/filter");
var DEFAULT_KEEP_FRESH_PERIOD = 600 * 1000; // 10分钟不刷新
;
var Cache = /** @class */ (function (_super) {
tslib_1.__extends(Cache, _super);
function Cache(storageSchema, aspectWrapper, frontendContextBuilder, checkers, getFullData, localStorage, savedEntities, keepFreshPeriod) {
var _this = _super.call(this) || this;
_this.refreshing = 0;
_this.refreshRecords = {};
_this.aspectWrapper = aspectWrapper;
_this.syncEventsCallbacks = [];
_this.cacheStore = new CacheStore_1.CacheStore(storageSchema);
_this.contextBuilder = function () { return frontendContextBuilder()(_this.cacheStore); };
_this.savedEntities = tslib_1.__spreadArray(['actionAuth', 'i18n'], tslib_1.__read((savedEntities || [])), false);
_this.keepFreshPeriod = keepFreshPeriod || DEFAULT_KEEP_FRESH_PERIOD;
_this.localStorage = localStorage;
checkers.forEach(function (checker) { return _this.cacheStore.registerChecker(checker); });
_this.getFullDataFn = getFullData;
_this.initSavedLogic();
return _this;
}
/**
* 处理cache中需要缓存的数据
*/
Cache.prototype.initSavedLogic = function () {
var _this = this;
var data = {};
this.savedEntities.forEach(function (entity) {
var key = "".concat(constant_1.LOCAL_STORAGE_KEYS.cacheSaved, ":").concat(entity);
var cached = _this.localStorage.load(key);
if (cached) {
data[entity] = cached;
}
});
this.cacheStore.resetInitialData(data);
this.cacheStore.onCommit(function (result) {
var entities = Object.keys(result);
var referenced = (0, lodash_1.intersection)(entities, _this.savedEntities);
if (referenced.length > 0) {
var saved_1 = _this.cacheStore.getCurrentData(referenced);
Object.keys(saved_1).forEach(function (entity) {
var key = "".concat(constant_1.LOCAL_STORAGE_KEYS.cacheSaved, ":").concat(entity);
_this.localStorage.save(key, saved_1[entity]);
});
}
});
};
Cache.prototype.getSchema = function () {
return this.cacheStore.getSchema();
};
/* getCurrentUserId(allowUnloggedIn?: boolean) {
const context = this.contextBuilder && this.contextBuilder();
return context?.getCurrentUserId(allowUnloggedIn);
} */
Cache.prototype.exec = function (name, params, callback, dontPublish) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var _a, result, opRecords, message, e_1, opRecord;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 2, , 3]);
this.refreshing++;
return [4 /*yield*/, this.aspectWrapper.exec(name, params)];
case 1:
_a = _b.sent(), result = _a.result, opRecords = _a.opRecords, message = _a.message;
if (opRecords) {
this.sync(opRecords);
}
this.refreshing--;
callback && callback(result, opRecords);
if (opRecords && opRecords.length > 0 && !dontPublish) {
this.publish();
}
return [2 /*return*/, {
result: result,
message: message,
}];
case 2:
e_1 = _b.sent();
// 如果是数据不一致错误,这里可以让用户知道
this.refreshing--;
if (e_1 instanceof Exception_1.OakException) {
opRecord = e_1.opRecord;
if (opRecord) {
this.sync([opRecord]);
this.publish();
}
}
throw e_1;
case 3: return [2 /*return*/];
}
});
});
};
Cache.prototype.getRefreshRecordSize = function () {
var _this = this;
var count = 0;
Object.keys(this.refreshRecords).forEach(function (entity) { return count += Object.keys(_this.refreshRecords[entity]).length; });
return count;
};
Cache.prototype.reduceRefreshRecord = function (now) {
var _this = this;
Object.keys(this.refreshRecords).forEach(function (ele) {
if (!_this.savedEntities.includes(ele)) {
var outdated = [];
for (var filter in _this.refreshRecords[ele]) {
if (_this.refreshRecords[ele][filter] < now - _this.keepFreshPeriod) {
outdated.push(filter);
}
}
_this.refreshRecords[ele] = (0, lodash_1.omit)(_this.refreshRecords[ele], outdated);
}
});
};
Cache.prototype.addRefreshRecord = function (entity, filter, now) {
var _a, _b, _c;
var _this = this;
var originTimestamp = this.refreshRecords[entity] && this.refreshRecords[entity][filter];
if (this.refreshRecords[entity]) {
Object.assign(this.refreshRecords[entity], (_a = {},
_a[filter] = now,
_a));
}
else {
Object.assign(this.refreshRecords, (_b = {},
_b[entity] = (_c = {},
_c[filter] = now,
_c),
_b));
}
var count = this.getRefreshRecordSize();
if (count > 100) {
count = this.getRefreshRecordSize();
this.reduceRefreshRecord(now);
if (count > 100) {
console.warn('cache中的refreshRecord数量过多请检查是否因为存在带有Date.now等变量的刷新例程', this.refreshRecords);
}
}
if (originTimestamp) {
return function () { return _this.addRefreshRecord(entity, filter, originTimestamp); };
}
};
/**
* 判定一个refresh行为是否可以应用缓存优化
* 可以优化的selection必须满足
* 1没有indexFrom和count
* 2没要求getCount
* 3查询的projection和filter只限定在该对象自身属性上
* 4有filter
* @param entity
* @param selection
* @param option
* @param getCount
*/
Cache.prototype.canOptimizeRefresh = function (entity, selection, option, getCount) {
var e_2, _a;
var _this = this;
if (getCount || selection.indexFrom || selection.count || selection.randomRange || !selection.filter || (option === null || option === void 0 ? void 0 : option.ignoreKeepFreshRule)) {
return false;
}
var data = selection.data, filter = selection.filter, sorter = selection.sorter;
// projection中不能有cascade查询或者表达式
var checkProjection = function (projection) {
for (var attr in data) {
var rel = _this.judgeRelation(entity, attr);
if (typeof rel !== 'number' || ![0, 1].includes(rel)) {
return false;
}
}
return true;
};
if (!checkProjection(data)) {
return false;
}
// filter中不能有cascade查询或者表达式
var checkFilter = function (filter2) {
var e_3, _a;
for (var attr in filter2) {
if (['$and', '$or'].includes(attr)) {
try {
for (var _b = (e_3 = void 0, tslib_1.__values(filter2[attr])), _c = _b.next(); !_c.done; _c = _b.next()) {
var f2 = _c.value;
if (!checkFilter(f2)) {
return false;
}
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_3) throw e_3.error; }
}
}
else if (attr === '$not') {
if (!checkFilter(filter2[attr])) {
return false;
}
}
else if (!attr.startsWith('$') || !attr.startsWith('#')) {
var rel = _this.judgeRelation(entity, attr);
if (typeof rel !== 'number' || ![0, 1].includes(rel)) {
return false;
}
}
}
return true;
};
if (!checkFilter(filter)) {
return false;
}
if (sorter) {
try {
for (var sorter_1 = tslib_1.__values(sorter), sorter_1_1 = sorter_1.next(); !sorter_1_1.done; sorter_1_1 = sorter_1.next()) {
var s = sorter_1_1.value;
if (!checkProjection(s.$attr)) {
return false;
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (sorter_1_1 && !sorter_1_1.done && (_a = sorter_1.return)) _a.call(sorter_1);
}
finally { if (e_2) throw e_2.error; }
}
}
return true;
};
Cache.prototype.filterToKey = function (filter) {
return JSON.stringify(filter);
};
Cache.prototype.refresh = function (entity, selection, option, getCount, callback, dontPublish, onlyReturnFresh) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var canOptimize, now, key, undoSetRefreshRecord, originFilter, filter, data, current, maxUpdateAt_1, ids, filter_2, _a, ids, count, aggr, filter2, selection2, data, err_1;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
canOptimize = this.canOptimizeRefresh(entity, selection, option, getCount);
now = 0, key = '';
if (canOptimize) {
filter = selection.filter;
(0, assert_1.default)(filter);
originFilter = filter;
key = this.filterToKey(filter);
now = Date.now();
if (this.refreshRecords[entity] && this.refreshRecords[entity][key]) {
if (this.refreshRecords[entity][key] > now - this.keepFreshPeriod && this.savedEntities.includes(entity)) {
// 对于savedEntities同一查询条件不必过于频繁刷新减少性能开销
if (process.env.NODE_ENV === 'development') {
// console.warn('根据keepFresh规则省略了一次请求数据的行为', entity, selection);
}
if (onlyReturnFresh) {
return [2 /*return*/, {
data: [],
}];
}
data = this.get(entity, selection);
return [2 /*return*/, {
data: data,
}];
}
else {
// 对于其它entity或者是过期的savedEntity可以加上增量条件只检查上次查询之后有更新的数据减少网络传输开销
if (!this.savedEntities.includes(entity) && process.env.NODE_ENV === 'development') {
console.log('对象的查询可能可以被缓存,请查看代码逻辑', entity);
}
selection.filter = (0, filter_1.combineFilters)(entity, this.getSchema(), [selection.filter, {
$$updateAt$$: {
$gte: this.refreshRecords[entity][key]
}
}]);
}
}
else if (this.savedEntities.includes(entity)) {
current = this.get(entity, {
data: {
id: 1,
$$updateAt$$: 1,
},
filter: filter,
});
if (current.length > 0) {
maxUpdateAt_1 = 0;
ids = current.map(function (row) {
if (typeof row.$$updateAt$$ === 'number' && row.$$updateAt$$ > maxUpdateAt_1) {
maxUpdateAt_1 = row.$$updateAt$$;
}
return row.id;
});
filter_2 = {
$or: [
{
id: {
$nin: ids,
},
},
{
$$updateAt$$: {
$gte: maxUpdateAt_1,
},
}
],
};
selection.filter = (0, filter_1.combineFilters)(entity, this.getSchema(), [filter_2, selection.filter]);
}
}
undoSetRefreshRecord = this.addRefreshRecord(entity, key, now);
}
_b.label = 1;
case 1:
_b.trys.push([1, 3, , 4]);
return [4 /*yield*/, this.exec('select', {
entity: entity,
selection: selection,
option: option,
getCount: getCount,
}, callback, dontPublish)];
case 2:
_a = (_b.sent()).result, ids = _a.ids, count = _a.count, aggr = _a.aggr;
filter2 = {
id: {
$in: ids,
}
};
if (canOptimize && !onlyReturnFresh) {
filter2 = originFilter;
}
selection2 = Object.assign({}, selection, {
filter: filter2,
});
data = this.get(entity, selection2);
if (aggr) {
(0, lodash_1.merge)(data, aggr);
}
return [2 /*return*/, {
data: data,
count: count,
}];
case 3:
err_1 = _b.sent();
undoSetRefreshRecord && undoSetRefreshRecord();
throw err_1;
case 4: return [2 /*return*/];
}
});
});
};
Cache.prototype.aggregate = function (entity, aggregation, option) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var result;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.aspectWrapper.exec('aggregate', {
entity: entity,
aggregation: aggregation,
option: option,
})];
case 1:
result = (_a.sent()).result;
return [2 /*return*/, result];
}
});
});
};
Cache.prototype.operate = function (entity, operation, option, callback) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var result;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.exec('operate', {
entity: entity,
operation: operation,
option: option,
}, callback)];
case 1:
result = _a.sent();
return [2 /*return*/, result];
}
});
});
};
Cache.prototype.count = function (entity, selection, option, callback) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var result;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.exec('count', {
entity: entity,
selection: selection,
option: option,
}, callback)];
case 1:
result = (_a.sent()).result;
return [2 /*return*/, result];
}
});
});
};
Cache.prototype.sync = function (records) {
// sync会异步并发的调用不能用this.context;
var context = this.contextBuilder();
this.cacheStore.sync(records, context);
// 唤起同步注册的回调
this.syncEventsCallbacks.map(function (ele) { return ele(records); });
};
/**
* 前端缓存做operation只可能是测试权限必然回滚
* @param entity
* @param operation
* @returns
*/
Cache.prototype.tryRedoOperations = function (operations) {
var e_4, _a;
this.begin();
try {
try {
for (var operations_1 = tslib_1.__values(operations), operations_1_1 = operations_1.next(); !operations_1_1.done; operations_1_1 = operations_1.next()) {
var oper = operations_1_1.value;
var entity = oper.entity, operation = oper.operation;
this.context.operate(entity, operation, {
dontCollect: true,
dontCreateOper: true,
dontCreateModi: true,
});
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (operations_1_1 && !operations_1_1.done && (_a = operations_1.return)) _a.call(operations_1);
}
finally { if (e_4) throw e_4.error; }
}
this.rollback();
return true;
}
catch (err) {
this.rollback();
if (!(err instanceof Exception_1.OakUserException)) {
throw err;
}
return err;
}
};
Cache.prototype.checkOperation = function (entity, action, data, filter, checkerTypes) {
var autoCommit = false;
if (!this.context) {
this.begin();
autoCommit = true;
}
var operation = {
action: action,
filter: filter,
data: 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;
}
};
Cache.prototype.redoOperation = function (opers) {
var _this = this;
(0, assert_1.default)(this.context);
opers.forEach(function (oper) {
var entity = oper.entity, operation = oper.operation;
_this.cacheStore.operate(entity, operation, _this.context, {
dontCollect: true,
dontCreateOper: true,
blockTrigger: true,
dontCreateModi: true,
});
});
return;
};
Cache.prototype.fetchRows = function (missedRows) {
var _this = this;
if (!this.refreshing) {
if (process.env.NODE_ENV === 'development') {
console.warn('缓存被动去获取数据,请查看页面行为并加以优化', missedRows);
}
this.exec('fetchRows', missedRows, function (result, opRecords) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var _a, _b, record, d, missedRows_1, missedRows_1_1, mr;
var e_5, _c, e_6, _d;
return tslib_1.__generator(this, function (_e) {
try {
// missedRows理论上一定要取到不能为空集。否则就是程序员有遗漏
for (_a = tslib_1.__values(opRecords), _b = _a.next(); !_b.done; _b = _a.next()) {
record = _b.value;
d = record.d;
(0, assert_1.default)(Object.keys(d).length > 0, '在通过fetchRow取不一致数据时返回了空数据请拿该程序员祭天。');
try {
for (missedRows_1 = (e_6 = void 0, tslib_1.__values(missedRows)), missedRows_1_1 = missedRows_1.next(); !missedRows_1_1.done; missedRows_1_1 = missedRows_1.next()) {
mr = missedRows_1_1.value;
(0, assert_1.default)(Object.keys(d[mr.entity]).length > 0, "\u5728\u901A\u8FC7fetchRow\u53D6\u4E0D\u4E00\u81F4\u6570\u636E\u65F6\u8FD4\u56DE\u4E86\u7A7A\u6570\u636E\uFF0C\u8BF7\u62FF\u8BE5\u7A0B\u5E8F\u5458\u796D\u5929\u3002entity\u662F".concat(mr.entity));
}
}
catch (e_6_1) { e_6 = { error: e_6_1 }; }
finally {
try {
if (missedRows_1_1 && !missedRows_1_1.done && (_d = missedRows_1.return)) _d.call(missedRows_1);
}
finally { if (e_6) throw e_6.error; }
}
}
}
catch (e_5_1) { e_5 = { error: e_5_1 }; }
finally {
try {
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
}
finally { if (e_5) throw e_5.error; }
}
return [2 /*return*/];
});
}); });
}
};
/**
* getById可以处理当本行不在缓存中的自动取
* @attention 这里如果访问了一个id不存在的行被删除可能会陷入无限循环。如果遇到了再处理
* @param entity
* @param data
* @param id
* @param allowMiss
*/
Cache.prototype.getById = function (entity, data, id, allowMiss) {
var result = this.getInner(entity, {
data: data,
filter: {
id: id,
},
}, allowMiss);
if (result.length === 0 && !allowMiss) {
this.fetchRows([{
entity: entity,
selection: {
data: data,
filter: {
id: id,
},
}
}]);
}
return result[0];
};
Cache.prototype.getInner = function (entity, selection, allowMiss) {
var autoCommit = false;
if (!this.context) {
this.begin();
autoCommit = true;
}
try {
var result = this.cacheStore.select(entity, selection, this.context, {
dontCollect: true,
includedDeleted: true,
});
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;
}
}
};
Cache.prototype.get = function (entity, selection, allowMiss) {
return this.getInner(entity, selection, allowMiss);
};
Cache.prototype.judgeRelation = function (entity, attr) {
return this.cacheStore.judgeRelation(entity, attr);
};
Cache.prototype.bindOnSync = function (callback) {
this.syncEventsCallbacks.push(callback);
};
Cache.prototype.unbindOnSync = function (callback) {
(0, lodash_1.pull)(this.syncEventsCallbacks, callback);
};
Cache.prototype.getCachedData = function () {
return this.cacheStore.getCurrentData();
};
Cache.prototype.getFullData = function () {
return this.getFullDataFn();
};
Cache.prototype.begin = function () {
(0, assert_1.default)(!this.context);
this.context = this.contextBuilder();
this.context.begin();
return this.context;
};
Cache.prototype.commit = function () {
(0, assert_1.default)(this.context);
this.context.commit();
this.context = undefined;
};
Cache.prototype.rollback = function () {
(0, assert_1.default)(this.context);
this.context.rollback();
this.context = undefined;
};
Cache.prototype.buildContext = function () {
return this.contextBuilder();
};
Cache.prototype.sub = function (data, callback) {
var _this = this;
return this.aspectWrapper.sub(data, function (records, ids) {
_this.sync(records),
callback(records, ids);
});
};
Cache.prototype.unsub = function (ids) {
return this.aspectWrapper.unsub(ids);
};
return Cache;
}(Feature_1.Feature));
exports.Cache = Cache;