2526 lines
93 KiB
JavaScript
2526 lines
93 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.RunningTree = exports.MODI_NEXT_PATH_SUFFIX = void 0;
|
||
const assert_1 = require("oak-domain/lib/utils/assert");
|
||
const lodash_1 = require("oak-domain/lib/utils/lodash");
|
||
const filter_1 = require("oak-domain/lib/store/filter");
|
||
const modi_1 = require("oak-domain/lib/store/modi");
|
||
const relation_1 = require("oak-domain/lib/store/relation");
|
||
const types_1 = require("oak-domain/lib/types");
|
||
const Feature_1 = require("../types/Feature");
|
||
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
||
exports.MODI_NEXT_PATH_SUFFIX = ':next';
|
||
const START_LSN = 100;
|
||
function mergeOperation(schema, entity, oper1, oper2) {
|
||
const { action, data } = oper2;
|
||
const operMerged = (0, lodash_1.cloneDeep)(oper1);
|
||
(0, assert_1.assert)(action !== 'create');
|
||
if (action === 'remove') {
|
||
if (oper1.action == 'create') {
|
||
// 操作抵消
|
||
return null;
|
||
}
|
||
else {
|
||
operMerged.action = 'remove';
|
||
operMerged.data = (0, lodash_1.cloneDeep)(data);
|
||
}
|
||
}
|
||
else {
|
||
if (action !== 'update') {
|
||
if (oper1.action === 'update') {
|
||
oper1.action = action;
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(action === oper1.action, '不应当有冲突的update模式');
|
||
}
|
||
}
|
||
const { data: dataMerged } = operMerged;
|
||
// merge 两者的data,要处理cascade部分
|
||
for (const attr in data) {
|
||
const rel = (0, relation_1.judgeRelation)(schema, entity, attr);
|
||
if (rel === 1) {
|
||
dataMerged[attr] = (0, lodash_1.cloneDeep)(data[attr]);
|
||
}
|
||
else if (rel === 2) {
|
||
if (dataMerged[attr]) {
|
||
const merged = mergeOperation(schema, attr, dataMerged[attr], data[attr]);
|
||
if (!merged) {
|
||
(0, lodash_1.unset)(dataMerged, attr);
|
||
}
|
||
else {
|
||
dataMerged[attr] = merged;
|
||
}
|
||
}
|
||
else {
|
||
dataMerged[attr] = (0, lodash_1.cloneDeep)(data[attr]);
|
||
}
|
||
}
|
||
else if (typeof rel === 'string') {
|
||
if (dataMerged[attr]) {
|
||
const merged = mergeOperation(schema, rel, dataMerged[attr], data[attr]);
|
||
if (!merged) {
|
||
(0, lodash_1.unset)(dataMerged, attr);
|
||
}
|
||
else {
|
||
dataMerged[attr] = (0, lodash_1.cloneDeep)(merged);
|
||
}
|
||
}
|
||
else {
|
||
dataMerged[attr] = data[attr];
|
||
}
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(rel instanceof Array);
|
||
if (!dataMerged[attr]) {
|
||
dataMerged[attr] = (0, lodash_1.cloneDeep)(data[attr]);
|
||
}
|
||
else {
|
||
// 这条路径在前台跑出来的情况现在还不好断言,先赋值,再调试
|
||
process.env.NODE_ENV === 'development' && console.warn('need check by Xc');
|
||
dataMerged[attr] = (0, lodash_1.cloneDeep)(data[attr]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return operMerged;
|
||
}
|
||
/**
|
||
* 树结点上的更新日志管理
|
||
*/
|
||
class UpdateLogManager {
|
||
logs = [];
|
||
schema;
|
||
entity;
|
||
maxLsn;
|
||
constructor(schema, entity, maxLsn) {
|
||
this.schema = schema,
|
||
this.entity = entity,
|
||
this.maxLsn = maxLsn || START_LSN;
|
||
}
|
||
/**
|
||
* 合并两个filter完全一致的更新
|
||
* @param oper1
|
||
* @param oper2
|
||
*/
|
||
mergeOperation(oper1, oper2) {
|
||
return mergeOperation(this.schema, this.entity, oper1, oper2);
|
||
}
|
||
/**
|
||
* 增加一条更新日志记录
|
||
* @param lsn
|
||
* @param action
|
||
* @param data
|
||
* @param filter
|
||
*/
|
||
push(lsn, oper2) {
|
||
// 插入到最后一次update中
|
||
const { action, filter, data } = oper2;
|
||
let ids = action === 'create' ? [data.id] : (0, filter_1.getRelevantIds)(filter);
|
||
const key = JSON.stringify(ids.length > 0 ? ids : filter);
|
||
if (this.logs.length > 0) {
|
||
const update = this.logs[this.logs.length - 1];
|
||
(0, assert_1.assert)(update.lsn <= lsn);
|
||
if (update.lsn === lsn) {
|
||
// 如果是同一个lsn,尝试合并
|
||
const { operations: logs } = update;
|
||
if (logs[key]) {
|
||
const newOper = this.mergeOperation(logs[key], oper2);
|
||
if (!newOper) {
|
||
(0, lodash_1.unset)(logs, key);
|
||
}
|
||
else {
|
||
logs[key] = newOper;
|
||
}
|
||
return;
|
||
}
|
||
logs[key] = {
|
||
id: (0, uuid_1.generateNewId)(),
|
||
action,
|
||
data,
|
||
filter,
|
||
};
|
||
return;
|
||
}
|
||
}
|
||
this.logs.push({
|
||
lsn,
|
||
operations: {
|
||
[key]: {
|
||
id: (0, uuid_1.generateNewId)(),
|
||
action,
|
||
data,
|
||
filter,
|
||
}
|
||
},
|
||
});
|
||
if (lsn > this.maxLsn) {
|
||
this.maxLsn = lsn;
|
||
}
|
||
}
|
||
undo(filter) {
|
||
const ids = (0, filter_1.getRelevantIds)(filter);
|
||
const key = JSON.stringify(ids.length > 0 ? ids : filter);
|
||
this.logs.forEach((log) => {
|
||
const { operations } = log;
|
||
(0, lodash_1.unset)(operations, key);
|
||
});
|
||
}
|
||
/**
|
||
* 将lsn大于传入lsn的日志全部回滚
|
||
* @param lsn
|
||
*/
|
||
rollback(lsn) {
|
||
const newLogs = this.logs.filter(ele => ele.lsn <= lsn);
|
||
this.logs = newLogs;
|
||
this.maxLsn = lsn || START_LSN;
|
||
}
|
||
makeOperations() {
|
||
const opers = {};
|
||
;
|
||
for (const log of this.logs) {
|
||
const { operations } = log;
|
||
for (const key in operations) {
|
||
if (opers[key]) {
|
||
const newOper = this.mergeOperation(opers[key], operations[key]);
|
||
if (newOper) {
|
||
opers[key] = newOper;
|
||
}
|
||
else {
|
||
(0, lodash_1.unset)(opers, key);
|
||
}
|
||
}
|
||
else {
|
||
opers[key] = (0, lodash_1.cloneDeep)(operations[key]);
|
||
}
|
||
}
|
||
}
|
||
return Object.values(opers);
|
||
}
|
||
isEmpty() {
|
||
return this.logs.length === 0;
|
||
}
|
||
}
|
||
class Node extends Feature_1.Feature {
|
||
zombie = false;
|
||
stale;
|
||
fullPath;
|
||
/**
|
||
* 当前有几个组件正在使用这个结点
|
||
*/
|
||
count = 0;
|
||
executing = false;
|
||
dirty;
|
||
extraData = {};
|
||
refreshing = 0;
|
||
refreshCallbacks = [];
|
||
constructor(fullPath) {
|
||
super();
|
||
this.fullPath = fullPath;
|
||
}
|
||
isInModiNextBranch() {
|
||
return this.fullPath.includes(exports.MODI_NEXT_PATH_SUFFIX);
|
||
}
|
||
increaseCount() {
|
||
this.count++;
|
||
return this.count;
|
||
}
|
||
decreaseCount() {
|
||
this.count--;
|
||
return this.count;
|
||
}
|
||
getCount() {
|
||
return this.count;
|
||
}
|
||
setZombie(zombie) {
|
||
this.zombie = zombie;
|
||
}
|
||
isZombie() {
|
||
return this.zombie;
|
||
}
|
||
isDirty() {
|
||
return !!this.dirty;
|
||
}
|
||
isLoading() {
|
||
return !!this.refreshing;
|
||
}
|
||
isExecuting() {
|
||
return this.executing;
|
||
}
|
||
setExecuting(executing) {
|
||
this.executing = executing;
|
||
this.publish();
|
||
}
|
||
startRefreshing(num) {
|
||
if (this.refreshing) {
|
||
return new Promise((resolve, reject) => {
|
||
this.refreshCallbacks.push([resolve, reject]);
|
||
});
|
||
}
|
||
this.refreshing = num;
|
||
for (const child in this.children) {
|
||
this.children[child].startRefreshing(num);
|
||
}
|
||
return num;
|
||
}
|
||
endRefreshing(num, error) {
|
||
/**
|
||
* 考虑树上可能有枝干是在父亲开始refresh之后才构造并开始自身的refresh
|
||
* 这里就用magicNumber来区分一下
|
||
*/
|
||
if (this.refreshing === num) {
|
||
this.refreshing = 0;
|
||
for (const child in this.children) {
|
||
this.children[child].endRefreshing(num, error);
|
||
}
|
||
const callbacks = this.refreshCallbacks;
|
||
this.refreshCallbacks = [];
|
||
callbacks.forEach(([resolve, reject]) => {
|
||
if (error) {
|
||
reject(error);
|
||
}
|
||
else {
|
||
resolve();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
saveExtraData(key, data) {
|
||
this.extraData[key] = data;
|
||
}
|
||
loadExtraData(key) {
|
||
return this.extraData[key];
|
||
}
|
||
isStale() {
|
||
return !!this.stale;
|
||
}
|
||
}
|
||
class EntityNode extends Node {
|
||
entity;
|
||
// protected fullPath: string;
|
||
schema;
|
||
projection; // 只在Page层有
|
||
parent;
|
||
cache;
|
||
// protected createData?: CreateDataDef<ED, T>;
|
||
ulManager;
|
||
constructor(entity, schema, cache, fullPath, projection, parent) {
|
||
super(fullPath);
|
||
this.entity = entity;
|
||
this.schema = schema;
|
||
this.cache = cache;
|
||
this.projection = projection;
|
||
this.parent = parent;
|
||
this.dirty = undefined;
|
||
this.executing = false;
|
||
this.ulManager = new UpdateLogManager(this.schema, this.entity);
|
||
}
|
||
getEntity() {
|
||
return this.entity;
|
||
}
|
||
getSchema() {
|
||
return this.schema;
|
||
}
|
||
setDirty() {
|
||
if (!this.dirty) {
|
||
this.dirty = true;
|
||
}
|
||
// 现在必须要将父结点都置dirty了再publish,因为整棵树是在一起redoOperation了
|
||
if (this.parent) {
|
||
this.parent.setDirty();
|
||
}
|
||
this.publish();
|
||
}
|
||
getParent() {
|
||
return this.parent;
|
||
}
|
||
getProjection() {
|
||
const projection = typeof this.projection === 'function' ? this.projection() : (this.projection && (0, lodash_1.cloneDeep)(this.projection));
|
||
return projection;
|
||
}
|
||
setProjection(projection) {
|
||
// assert(!this.projection);
|
||
this.projection = projection;
|
||
}
|
||
judgeRelation(attr) {
|
||
const attr2 = attr.split(':')[0]; // 处理attr:next
|
||
return (0, relation_1.judgeRelation)(this.schema, this.entity, attr2);
|
||
}
|
||
}
|
||
const DEFAULT_PAGINATION = {
|
||
currentPage: 0,
|
||
pageSize: 20,
|
||
more: true,
|
||
total: undefined,
|
||
count: 0,
|
||
};
|
||
class ListNode extends EntityNode {
|
||
children = {};
|
||
filters;
|
||
sorters;
|
||
getTotal;
|
||
pagination = { ...DEFAULT_PAGINATION };
|
||
sr = {};
|
||
loadingMore = false;
|
||
syncHandler;
|
||
setFiltersAndSortedApplied() {
|
||
this.filters.forEach(ele => ele.applied = true);
|
||
this.sorters.forEach(ele => ele.applied = true);
|
||
for (const k in this.children) {
|
||
this.children[k].setFiltersAndSortedApplied();
|
||
}
|
||
}
|
||
startLoading() {
|
||
const number = Math.random() + 1;
|
||
return this.startRefreshing(number);
|
||
}
|
||
checkIfClean(child) {
|
||
if (!this.ulManager.isEmpty()) {
|
||
return false;
|
||
}
|
||
for (const k in this.children) {
|
||
if (this.children[k] === child) {
|
||
continue;
|
||
}
|
||
if (this.children[k].isDirty()) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
getModiOperations() {
|
||
if (this.schema[this.entity].toModi) {
|
||
const ids = Object.keys(this.sr);
|
||
if (ids.length > 0) {
|
||
const modies = this.cache.get('modi', {
|
||
data: {
|
||
id: 1,
|
||
targetEntity: 1,
|
||
entity: 1,
|
||
entityId: 1,
|
||
iState: 1,
|
||
action: 1,
|
||
data: 1,
|
||
filter: 1,
|
||
},
|
||
filter: {
|
||
entity: this.entity,
|
||
entityId: {
|
||
$in: ids,
|
||
},
|
||
iState: 'active',
|
||
},
|
||
sorter: [
|
||
{
|
||
$attr: {
|
||
$$createAt$$: 1,
|
||
},
|
||
$direction: 'asc',
|
||
}
|
||
],
|
||
});
|
||
if (modies.length > 0) {
|
||
return (0, modi_1.createOperationsFromModies)(modies);
|
||
}
|
||
}
|
||
}
|
||
const modiOperations = [];
|
||
for (const c in this.children) {
|
||
const child = this.children[c];
|
||
const result = child.getModiOperations();
|
||
if (result) {
|
||
modiOperations.push(...result);
|
||
}
|
||
}
|
||
if (modiOperations.length > 0) {
|
||
return modiOperations;
|
||
}
|
||
}
|
||
onCacheSync(records) {
|
||
if (this.isLoading()) {
|
||
return;
|
||
}
|
||
const tryAddRowToList = (id, filter) => {
|
||
if (this.sr[id]) {
|
||
return;
|
||
}
|
||
if (!filter || this.cache.checkFilterContains(this.entity, filter, {
|
||
id,
|
||
}, true)) {
|
||
this.sr[id] = {}; // 如果有aggr怎么办,一般有aggr的页面不会出现这种情况,以后再说
|
||
if (typeof this.pagination.total === 'number') {
|
||
this.pagination.total += 1;
|
||
}
|
||
if (typeof this.pagination.count === 'number') {
|
||
this.pagination.count += 1;
|
||
}
|
||
}
|
||
};
|
||
// let hasUpdated = false;
|
||
for (const record of records) {
|
||
const { a } = record;
|
||
switch (a) {
|
||
case 'c': {
|
||
const { e, d } = record;
|
||
if (e === this.entity) {
|
||
const filters = this.constructFilters(true, true, true);
|
||
const filter = filters && (0, filter_1.combineFilters)(e, this.schema, filters);
|
||
if (d instanceof Array) {
|
||
d.forEach((dd) => tryAddRowToList(dd.id, filter));
|
||
}
|
||
else {
|
||
tryAddRowToList(d.id, filter);
|
||
}
|
||
}
|
||
// hasUpdated = true;
|
||
break;
|
||
}
|
||
case 'r': {
|
||
const { e, f } = record;
|
||
if (e === this.entity) {
|
||
if (!f) {
|
||
this.sr = {};
|
||
this.pagination.total = 0;
|
||
}
|
||
else if (f.id) {
|
||
// 绝大多数删除情况
|
||
// 后台一定会返回删除的ids
|
||
(0, assert_1.assert)(f.id.$in instanceof Array);
|
||
f.id.$in.forEach((id) => {
|
||
if (this.sr.hasOwnProperty(id)) {
|
||
(0, lodash_1.unset)(this.sr, id);
|
||
if (typeof this.pagination.total === 'number') {
|
||
this.pagination.total -= 1;
|
||
}
|
||
if (typeof this.pagination.count === 'number') {
|
||
this.pagination.count -= 1;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
for (const id in this.sr) {
|
||
if (!f || this.cache.checkFilterContains(e, f, { id }, true)) {
|
||
(0, lodash_1.unset)(this.sr, id);
|
||
if (typeof this.pagination.total === 'number') {
|
||
this.pagination.total -= 1;
|
||
}
|
||
if (typeof this.pagination.count === 'number') {
|
||
this.pagination.count -= 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// hasUpdated = true;
|
||
break;
|
||
}
|
||
case 's': {
|
||
const { d } = record;
|
||
for (const entity in d) {
|
||
if (entity === this.entity) {
|
||
const filters = this.constructFilters(true, true, true);
|
||
const filter = filters && (0, filter_1.combineFilters)(entity, this.schema, filters);
|
||
for (const id in d[entity]) {
|
||
tryAddRowToList(id, filter);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case 'u':
|
||
default: {
|
||
// 更新可能会引起本行不再满足条件
|
||
const { e, d, f } = record;
|
||
if (e === this.entity) {
|
||
const { id } = f;
|
||
const ids = typeof id === 'string' ? [id] : id.$in;
|
||
const currentIds = Object.keys(this.sr);
|
||
const intersected = (0, lodash_1.intersection)(ids, currentIds);
|
||
const diffed = (0, lodash_1.difference)(ids, currentIds);
|
||
/** 检查原本就在的,现在还在不在,
|
||
* 以及原本不在的,现在是不是满足条件了
|
||
* (后一种情况也可能原本就满足但不在当前的ids中,这时是判断不出来的,total+1可能不对,但是应该是较少的case)*/
|
||
const filter = this.constructFilters(true, true, true);
|
||
if (intersected.length) {
|
||
const rows = this.cache.get(this.entity, {
|
||
data: {
|
||
id: 1,
|
||
},
|
||
filter: (0, filter_1.combineFilters)(this.entity, this.schema, [
|
||
...(filter || []),
|
||
{
|
||
id: {
|
||
$in: intersected,
|
||
}
|
||
}
|
||
])
|
||
});
|
||
const rowIds = rows.map(ele => ele.id);
|
||
const missed = (0, lodash_1.difference)(intersected, rowIds);
|
||
missed.forEach((id) => {
|
||
(0, lodash_1.unset)(this.sr, id);
|
||
if (this.pagination.total) {
|
||
this.pagination.total--;
|
||
}
|
||
});
|
||
}
|
||
if (diffed.length) {
|
||
diffed.forEach((ele) => {
|
||
this.sr[ele] = {};
|
||
if (this.pagination.total) {
|
||
this.pagination.total++;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
// hasUpdated = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// 现在在getFreshValue时,会判定this.sr中的id是否满足条件,所以不用再处理update行为了
|
||
/* if (hasUpdated) {
|
||
const ids = Object.keys(this.sr);
|
||
const { filter } = this.constructSelection(true, true, true);
|
||
if (filter && ids.length > 0) {
|
||
const filter2 = combineFilters(this.entity, this.cache.getSchema(), [filter, {
|
||
id: {
|
||
$in: ids,
|
||
}
|
||
}]);
|
||
|
||
const rows = this.cache.get(this.entity, {
|
||
data: {
|
||
id: 1,
|
||
},
|
||
filter: filter2,
|
||
});
|
||
|
||
const ids2 = rows.map(ele => ele.id!);
|
||
|
||
const illegalIds = difference(ids, ids2);
|
||
|
||
illegalIds.forEach(
|
||
(id) => unset(this.sr, id)
|
||
);
|
||
}
|
||
} */
|
||
}
|
||
destroy() {
|
||
this.cache.unbindOnSync(this.syncHandler);
|
||
for (const k in this.children) {
|
||
this.children[k].destroy();
|
||
}
|
||
}
|
||
constructor(entity, schema, cache, fullPath, projection, parent, path, filters, sorters, getTotal, pagination, stale) {
|
||
super(entity, schema, cache, fullPath, projection, parent);
|
||
this.filters = filters || [];
|
||
this.sorters = sorters || [];
|
||
this.getTotal = getTotal;
|
||
this.sr = {};
|
||
this.pagination = pagination ? {
|
||
...pagination,
|
||
currentPage: pagination.currentPage - 1,
|
||
more: true,
|
||
total: undefined,
|
||
count: 0,
|
||
getTotal
|
||
} : { ...DEFAULT_PAGINATION, getTotal };
|
||
this.stale = stale;
|
||
this.syncHandler = (records) => this.onCacheSync(records);
|
||
this.cache.bindOnSync(this.syncHandler);
|
||
if (parent) {
|
||
(0, assert_1.assert)(path);
|
||
parent.addChild(path, this);
|
||
}
|
||
}
|
||
getPagination() {
|
||
return this.pagination;
|
||
}
|
||
setPagination(pagination, dontRefresh) {
|
||
const newPagination = Object.assign(this.pagination, pagination);
|
||
this.pagination = newPagination;
|
||
if (!dontRefresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
}
|
||
isLoadingMore() {
|
||
return this.loadingMore;
|
||
}
|
||
addChild(path, node) {
|
||
(0, assert_1.assert)(!this.children[path]);
|
||
// assert(path.length > 10, 'List的path改成了id');
|
||
this.children[path] = node;
|
||
(0, assert_1.assert)(this.sr); // listNode的子结点不可能在取得数据前就建立吧 by Xc
|
||
node.saveRefreshResult({
|
||
[path]: this.sr[path] || {},
|
||
});
|
||
}
|
||
removeChild(path) {
|
||
(0, lodash_1.unset)(this.children, path);
|
||
}
|
||
getChild(path) {
|
||
return this.children[path];
|
||
}
|
||
getNamedFilters() {
|
||
return this.filters;
|
||
}
|
||
getNamedFilterByName(name) {
|
||
const filter = this.filters.find((ele) => ele['#name'] === name);
|
||
return filter;
|
||
}
|
||
setNamedFilters(filters, refresh) {
|
||
this.filters = filters.map((ele) => Object.assign({}, ele, { applied: false }));
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
addNamedFilter(filter, refresh) {
|
||
// filter 根据#name查找
|
||
const fIndex = this.filters.findIndex((ele) => filter['#name'] && ele['#name'] === filter['#name']);
|
||
if (fIndex >= 0) {
|
||
this.filters.splice(fIndex, 1, Object.assign({}, filter, { applied: false }));
|
||
}
|
||
else {
|
||
this.filters.push(Object.assign({}, filter, { applied: false }));
|
||
}
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
removeNamedFilter(filter, refresh) {
|
||
// filter 根据#name查找
|
||
const fIndex = this.filters.findIndex((ele) => filter['#name'] && ele['#name'] === filter['#name']);
|
||
if (fIndex >= 0) {
|
||
this.filters.splice(fIndex, 1);
|
||
}
|
||
this.pagination.total = undefined;
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
removeNamedFilterByName(name, refresh) {
|
||
// filter 根据#name查找
|
||
const fIndex = this.filters.findIndex((ele) => ele['#name'] === name);
|
||
if (fIndex >= 0) {
|
||
this.filters.splice(fIndex, 1);
|
||
}
|
||
this.pagination.total = undefined;
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
getNamedSorters() {
|
||
return this.sorters;
|
||
}
|
||
getNamedSorterByName(name) {
|
||
const sorter = this.sorters.find((ele) => ele && ele['#name'] === name);
|
||
return sorter;
|
||
}
|
||
setNamedSorters(sorters, refresh) {
|
||
this.sorters = sorters.map(ele => Object.assign({}, ele, { applied: false }));
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
addNamedSorter(sorter, refresh) {
|
||
// sorter 根据#name查找
|
||
const fIndex = this.sorters.findIndex((ele) => sorter['#name'] && ele['#name'] === sorter['#name']);
|
||
if (fIndex >= 0) {
|
||
this.sorters.splice(fIndex, 1, Object.assign({}, sorter, { applied: false }));
|
||
}
|
||
else {
|
||
this.sorters.push(Object.assign({}, sorter, { applied: false }));
|
||
}
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
removeNamedSorter(sorter, refresh) {
|
||
// sorter 根据#name查找
|
||
const fIndex = this.sorters.findIndex((ele) => sorter['#name'] && ele['#name'] === sorter['#name']);
|
||
if (fIndex >= 0) {
|
||
this.sorters.splice(fIndex, 1);
|
||
}
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
removeNamedSorterByName(name, refresh) {
|
||
// sorter 根据#name查找
|
||
const fIndex = this.sorters.findIndex((ele) => ele['#name'] === name);
|
||
if (fIndex >= 0) {
|
||
this.sorters.splice(fIndex, 1);
|
||
}
|
||
if (refresh) {
|
||
this.refresh(0, false);
|
||
}
|
||
else {
|
||
this.publish();
|
||
}
|
||
}
|
||
getFreshValue() {
|
||
/**
|
||
* 现在简化情况,只取sr中有id的数据,以及addItem中的create数据
|
||
* 但是对于modi查询,需要查询“热更新”部分的数据(因为这部分数据不会被sync到内存中,逻辑不严密,后面再说)
|
||
*/
|
||
const inModiNextBranch = this.isInModiNextBranch();
|
||
const ids = Object.keys(this.sr);
|
||
const operations = this.ulManager.makeOperations();
|
||
const createIds = operations.filter(o => o.action === 'create').map(o => o.data.id);
|
||
const { data, sorter, filter } = this.constructSelection(true, false, true, true);
|
||
const ids2 = ids.concat(createIds);
|
||
/**
|
||
* 在非modi状态下,当前的逻辑是用当前filter加以过滤,不然就处理不了在列表中操作改变了filter中外联的条件(例如为order创建了一个ship,但查询条件是没有ship)时,列表不发生变化
|
||
* 所以在这里当前是要求用户在取数据的时候,将可能影响的这种外联的数据也取到cache当中
|
||
* by Xc 20250315
|
||
*/
|
||
const filter2 = inModiNextBranch ? filter : (ids2.length > 0 ? (filter ? (0, filter_1.combineFilters)(this.entity, this.cache.getSchema(), [filter, {
|
||
id: {
|
||
$in: ids2,
|
||
}
|
||
}]) : {
|
||
id: {
|
||
$in: ids2,
|
||
}
|
||
}) : undefined);
|
||
if (filter2 && data) {
|
||
let sr = {
|
||
...this.sr,
|
||
};
|
||
if (createIds.length) {
|
||
createIds.forEach((id) => Object.assign(sr, {
|
||
[id]: {},
|
||
}));
|
||
}
|
||
const result = this.cache.get(this.entity, {
|
||
data,
|
||
filter: inModiNextBranch ? filter : filter2,
|
||
sorter,
|
||
}, inModiNextBranch ? undefined : sr);
|
||
return result;
|
||
}
|
||
return [];
|
||
}
|
||
addItemInner(item, lsn) {
|
||
// 如果数据键值是一个空字符串则更新成null
|
||
for (const k in item) {
|
||
if (item[k] === '') {
|
||
Object.assign(item, {
|
||
[k]: null,
|
||
});
|
||
}
|
||
}
|
||
const id = item.id || (0, uuid_1.generateNewId)();
|
||
const now = Date.now();
|
||
this.ulManager.push(lsn, {
|
||
action: 'create',
|
||
data: Object.assign(item, {
|
||
id,
|
||
[types_1.CreateAtAttribute]: now,
|
||
[types_1.UpdateAtAttribute]: now,
|
||
}),
|
||
});
|
||
return id;
|
||
}
|
||
addItem(lsn, item) {
|
||
const id = this.addItemInner(item, lsn);
|
||
this.setDirty();
|
||
return id;
|
||
}
|
||
addItems(lsn, items) {
|
||
const ids = items.map((item) => this.addItemInner(item, lsn));
|
||
this.setDirty();
|
||
return ids;
|
||
}
|
||
removeItemInner(id, lsn) {
|
||
this.ulManager.push(lsn, {
|
||
action: 'remove',
|
||
data: {
|
||
[types_1.DeleteAtAttribute]: Date.now(),
|
||
},
|
||
filter: {
|
||
id,
|
||
},
|
||
});
|
||
}
|
||
removeItem(lsn, id) {
|
||
this.removeItemInner(id, lsn);
|
||
this.setDirty();
|
||
}
|
||
removeItems(lsn, ids) {
|
||
ids.forEach((id) => this.removeItemInner(id, lsn));
|
||
this.setDirty();
|
||
}
|
||
recoverItemInner(id) {
|
||
this.ulManager.undo({ id });
|
||
}
|
||
recoverItem(id) {
|
||
this.recoverItemInner(id);
|
||
this.setDirty();
|
||
}
|
||
recoverItems(ids) {
|
||
ids.forEach((id) => this.recoverItemInner(id));
|
||
this.setDirty();
|
||
}
|
||
resetItem(id) {
|
||
this.ulManager.undo({ id });
|
||
}
|
||
preProcessUpdateData(data) {
|
||
const undefinedAttrs = [];
|
||
// 如果数据键值是一个空字符串则更新成null,undefined则unset掉
|
||
for (const k in data) {
|
||
if (data[k] === '' && !this.schema[this.entity].attributes[k].notNull) {
|
||
Object.assign(data, {
|
||
[k]: null,
|
||
});
|
||
}
|
||
else if (data[k] === undefined) {
|
||
undefinedAttrs.push(k);
|
||
}
|
||
}
|
||
undefinedAttrs.forEach((attr) => (0, lodash_1.unset)(data, attr));
|
||
}
|
||
updateItemInner(lsn, data, id, action) {
|
||
this.ulManager.push(lsn, {
|
||
action: action || 'update',
|
||
data: Object.assign({}, data, {
|
||
[types_1.UpdateAtAttribute]: Date.now(),
|
||
}),
|
||
filter: {
|
||
id,
|
||
},
|
||
});
|
||
}
|
||
/**
|
||
* 目前只支持根据itemId进行更新
|
||
* @param data
|
||
* @param id
|
||
* @param beforeExecute
|
||
* @param afterExecute
|
||
*/
|
||
updateItem(lsn, data, id, action) {
|
||
this.preProcessUpdateData(data);
|
||
this.updateItemInner(lsn, data, id, action);
|
||
this.setDirty();
|
||
}
|
||
updateItems(lsn, data, ids, action) {
|
||
this.preProcessUpdateData(data);
|
||
ids.forEach((id) => this.updateItemInner(lsn, data, id, action));
|
||
this.setDirty();
|
||
}
|
||
composeOperations(paths) {
|
||
if (!this.dirty) {
|
||
return;
|
||
}
|
||
const intrinsticFilter = this.getIntrinsticFilters();
|
||
const ulmLsn = this.ulManager.maxLsn;
|
||
paths?.shift();
|
||
for (const id in this.children) {
|
||
const childOperations = this.children[id].composeOperations(paths);
|
||
if (childOperations) {
|
||
childOperations.forEach((childOperation) => this.ulManager.push(ulmLsn + 100, childOperation.operation));
|
||
}
|
||
}
|
||
const operations = this.ulManager.makeOperations();
|
||
this.ulManager.rollback(ulmLsn);
|
||
return operations.map((operation) => {
|
||
const { filter, ...rest } = operation;
|
||
return {
|
||
entity: this.entity,
|
||
/* 现在update有一对多或者多对一的外键对象,oak-db会报错,暂时先封掉
|
||
operation: {
|
||
...rest,
|
||
// 加上instrinstic filter有机率降低后台各种检查对数据库的访问(先决条件已经满足约束)
|
||
filter: combineFilters(this.entity, this.schema, [filter, intrinsticFilter]),
|
||
} */
|
||
operation
|
||
};
|
||
});
|
||
}
|
||
getProjection() {
|
||
const projection = super.getProjection();
|
||
// List必须自主决定Projection
|
||
/* if (this.children.length > 0) {
|
||
const subProjection = await this.children[0].getProjection();
|
||
return merge(projection, subProjection);
|
||
} */
|
||
return projection;
|
||
}
|
||
constructFilters(withParent, ignoreNewParent, ignoreUnapplied, onlyHot) {
|
||
const { filters: ownFilters } = this;
|
||
const filters = ownFilters.filter(ele => (!ignoreUnapplied || ele.applied === true || ele.applied === undefined) // 如果是undefined,说明不可以移除(构造时就存在),也得返回
|
||
&& (!onlyHot || ele.hot) // 如果是getFreshValue,则只取hot的敏感过滤条件
|
||
).map((ele) => {
|
||
const { filter } = ele;
|
||
if (typeof filter === 'function') {
|
||
return filter();
|
||
}
|
||
return filter;
|
||
});
|
||
if (withParent && this.parent) {
|
||
if (this.parent instanceof SingleNode) {
|
||
// @ts-ignore
|
||
const filterOfParent = this.parent.getParentFilter(this, ignoreNewParent);
|
||
if (filterOfParent) {
|
||
filters.push(filterOfParent);
|
||
}
|
||
else {
|
||
// 说明有父结点但是却没有相应的约束,此时不应该去refresh(是一个insert动作)
|
||
return undefined;
|
||
}
|
||
}
|
||
}
|
||
// 返回的filter在上层做check的时候可能被改造掉
|
||
return (0, lodash_1.cloneDeep)(filters);
|
||
}
|
||
constructSelection(withParent, ignoreNewParent, ignoreUnapplied, onlyHot) {
|
||
const { sorters, getTotal } = this;
|
||
const data = this.getProjection();
|
||
// assert(data, '取数据时找不到projection信息');
|
||
const sorterArr = sorters.length > 0 ? sorters.filter(ele => !ignoreUnapplied || ele.applied).map((ele) => {
|
||
const { sorter } = ele;
|
||
if (typeof sorter === 'function') {
|
||
return sorter();
|
||
}
|
||
return sorter;
|
||
}).flat().filter((ele) => !!ele) : [
|
||
{
|
||
$attr: {
|
||
$$createAt$$: 1,
|
||
},
|
||
$direction: 'desc'
|
||
}
|
||
];
|
||
const filters = this.constructFilters(withParent, ignoreNewParent, ignoreUnapplied, onlyHot);
|
||
const filters2 = filters?.filter((ele) => !!ele);
|
||
const filter = filters2 ? (0, filter_1.combineFilters)(this.entity, this.schema, filters2) : undefined;
|
||
const { currentPage, pageSize } = this.pagination;
|
||
// 只有当没有计算过total或者有新的filter才需要显示的查找total
|
||
const needGetTotal = this.pagination.total === undefined || !!this.filters.find(ele => !ele.applied);
|
||
return {
|
||
data,
|
||
filter,
|
||
sorter: sorterArr,
|
||
total: needGetTotal ? getTotal : undefined,
|
||
indexFrom: currentPage * pageSize,
|
||
count: pageSize,
|
||
};
|
||
}
|
||
/**
|
||
* 存留查询结果
|
||
*/
|
||
saveRefreshResult(sr, append, currentPage) {
|
||
const { data, total } = sr;
|
||
(0, assert_1.assert)(data);
|
||
const count = Object.keys(data).length;
|
||
const getTotal = this.getTotal;
|
||
this.pagination.more = count === this.pagination.pageSize;
|
||
if (typeof currentPage === 'number' && !append) {
|
||
this.pagination.currentPage = currentPage;
|
||
}
|
||
if (typeof total === 'number') {
|
||
this.pagination.total = total;
|
||
}
|
||
if (typeof getTotal === 'number') {
|
||
const total2 = this.pagination.total || 0;
|
||
if (this.pagination.more && total2 < getTotal) {
|
||
const pages = Math.floor((total2 - 1) / this.pagination.pageSize) + 1;
|
||
this.pagination.more = this.pagination.currentPage + 1 < pages;
|
||
}
|
||
}
|
||
if (append) {
|
||
this.pagination.count += count;
|
||
this.sr = {
|
||
...this.sr,
|
||
...data,
|
||
};
|
||
}
|
||
else {
|
||
this.pagination.count = count;
|
||
this.sr = data || {};
|
||
}
|
||
for (const k in this.children) {
|
||
const child = this.children[k];
|
||
child.saveRefreshResult({
|
||
[k]: this.sr[k] || {},
|
||
});
|
||
}
|
||
// this.publish();
|
||
}
|
||
async refresh(pageNumber, append, resetTotal) {
|
||
if (resetTotal) {
|
||
// 清空之前查询计算的total值,目前只有父结点id改变会这样调用
|
||
this.pagination.total = undefined;
|
||
}
|
||
const { entity, pagination } = this;
|
||
const { currentPage, pageSize, randomRange } = pagination;
|
||
const currentPage3 = typeof pageNumber === 'number' ? pageNumber : currentPage;
|
||
(0, assert_1.assert)(!randomRange || !currentPage3, 'list在访问数据时,如果设置了randomRange,则不应再有pageNumber');
|
||
const { data: projection, filter, sorter, total, } = this.constructSelection(true, true);
|
||
// 若不存在有效的过滤条件(若有父结点但却为空时,说明父结点是一个create动作,不用刷新),则不能刷新
|
||
if ((!this.getParent() || this.getParent() instanceof VirtualNode || filter) && projection) {
|
||
const lr = !append && this.startLoading();
|
||
if (lr instanceof Promise) {
|
||
return lr;
|
||
}
|
||
try {
|
||
if (append) {
|
||
this.loadingMore = true;
|
||
}
|
||
this.publish();
|
||
await this.cache.refresh(entity, {
|
||
data: projection,
|
||
filter,
|
||
sorter,
|
||
indexFrom: currentPage3 * pageSize,
|
||
count: pageSize,
|
||
randomRange,
|
||
total,
|
||
}, undefined, (selectResult) => {
|
||
typeof lr === 'number' && this.endRefreshing(lr);
|
||
this.setFiltersAndSortedApplied();
|
||
if (append) {
|
||
this.loadingMore = false;
|
||
}
|
||
// 这里不能publish,因为
|
||
this.saveRefreshResult(selectResult, append, currentPage3);
|
||
});
|
||
}
|
||
catch (err) {
|
||
typeof lr === 'number' && this.endRefreshing(lr, err);
|
||
if (append) {
|
||
this.loadingMore = false;
|
||
}
|
||
this.publish();
|
||
throw err;
|
||
}
|
||
}
|
||
else {
|
||
// 不刷新也publish一下,触发页面reRender,不然有可能导致页面不进入formData
|
||
this.setFiltersAndSortedApplied();
|
||
this.sr = {};
|
||
this.publish();
|
||
}
|
||
}
|
||
async loadMore() {
|
||
const { filters, sorters, pagination, entity } = this;
|
||
const { pageSize, more, currentPage, count } = pagination;
|
||
if (!more || this.loadingMore || !count) {
|
||
return;
|
||
}
|
||
// 要根据total来计算下一页
|
||
(0, assert_1.assert)(count);
|
||
const currentPage2 = count / pageSize;
|
||
await this.refresh(currentPage2, true);
|
||
}
|
||
setCurrentPage(currentPage) {
|
||
this.refresh(currentPage, false);
|
||
}
|
||
clean(lsn, dontPublish) {
|
||
for (const k in this.children) {
|
||
this.children[k].clean(lsn, true);
|
||
}
|
||
if (this.dirty) {
|
||
this.ulManager.rollback(lsn || 0);
|
||
if (!lsn) {
|
||
this.dirty = undefined;
|
||
}
|
||
if (!dontPublish) {
|
||
this.publish();
|
||
}
|
||
}
|
||
}
|
||
// 查看这个list上所有数据必须遵守的限制
|
||
getIntrinsticFilters() {
|
||
const filters = this.constructFilters(undefined, true, true);
|
||
return (0, filter_1.combineFilters)(this.entity, this.schema, filters || []);
|
||
}
|
||
}
|
||
class SingleNode extends EntityNode {
|
||
id;
|
||
sr;
|
||
children;
|
||
filters;
|
||
constructor(entity, schema, cache, fullPath, path, projection, parent, id, filters, stale) {
|
||
super(entity, schema, cache, fullPath, projection, parent);
|
||
this.children = {};
|
||
this.sr = {};
|
||
this.filters = filters;
|
||
this.stale = stale;
|
||
// addChild有可能为本结点赋上id值,所以要先行
|
||
if (parent) {
|
||
(0, assert_1.assert)(path);
|
||
parent.addChild(path, this);
|
||
}
|
||
if (id) {
|
||
if (this.id) {
|
||
(0, assert_1.assert)(id === this.id, 'singleNode初始化的id必须一致');
|
||
}
|
||
else {
|
||
this.id = id;
|
||
}
|
||
}
|
||
}
|
||
getModiOperations() {
|
||
if (this.schema[this.entity].toModi) {
|
||
const modies = this.cache.get('modi', {
|
||
data: {
|
||
id: 1,
|
||
targetEntity: 1,
|
||
entity: 1,
|
||
entityId: 1,
|
||
iState: 1,
|
||
action: 1,
|
||
data: 1,
|
||
filter: 1,
|
||
},
|
||
filter: {
|
||
entity: this.entity,
|
||
entityId: this.id,
|
||
iState: 'active',
|
||
},
|
||
sorter: [
|
||
{
|
||
$attr: {
|
||
$$createAt$$: 1,
|
||
},
|
||
$direction: 'asc',
|
||
}
|
||
],
|
||
});
|
||
if (modies.length > 0) {
|
||
return (0, modi_1.createOperationsFromModies)(modies);
|
||
}
|
||
}
|
||
// 当前假设设计中不存在modi嵌套modi的情况,找到一个即可返回
|
||
for (const c in this.children) {
|
||
const child = this.children[c];
|
||
if (child instanceof SingleNode) {
|
||
const result = child.getModiOperations();
|
||
if (result) {
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
setFiltersAndSortedApplied() {
|
||
for (const k in this.children) {
|
||
this.children[k].setFiltersAndSortedApplied();
|
||
}
|
||
}
|
||
startLoading() {
|
||
const number = Math.random() + 1;
|
||
return this.startRefreshing(number);
|
||
}
|
||
checkIfClean(node) {
|
||
if (!this.ulManager.isEmpty()) {
|
||
return false;
|
||
}
|
||
for (const k in this.children) {
|
||
if (this.children[k] === node) {
|
||
continue;
|
||
}
|
||
if (this.children[k].isDirty()) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
destroy() {
|
||
for (const k in this.children) {
|
||
this.children[k].destroy();
|
||
}
|
||
}
|
||
getChild(path) {
|
||
return this.children[path];
|
||
}
|
||
setId(id) {
|
||
if (id !== this.id) {
|
||
const operations = this.ulManager.makeOperations();
|
||
(0, assert_1.assert)(operations.length <= 1);
|
||
const [operation] = operations;
|
||
if (operation?.action === 'create') {
|
||
if (operation.data.id === id) {
|
||
// 如果本身是create, 这里无视就行(因为框架原因会调用一次)
|
||
return;
|
||
}
|
||
else {
|
||
// 这种情况是oakId属性没有初始化完成
|
||
this.clean(0, true);
|
||
}
|
||
}
|
||
(0, assert_1.assert)(!this.dirty, 'setId时结点是dirty,在setId之前应当处理掉原有的update');
|
||
this.id = id;
|
||
this.refreshListChildren();
|
||
this.publish();
|
||
}
|
||
}
|
||
unsetId() {
|
||
if (this.id) {
|
||
this.id = undefined;
|
||
this.refreshListChildren();
|
||
this.publish();
|
||
}
|
||
}
|
||
getId() {
|
||
if (this.id) {
|
||
return this.id;
|
||
}
|
||
const operations = this.ulManager.makeOperations();
|
||
(0, assert_1.assert)(operations.length <= 1);
|
||
const [operation] = operations;
|
||
if (operation && operation?.action === 'create') {
|
||
return operation.data.id;
|
||
}
|
||
}
|
||
getChildren() {
|
||
return this.children;
|
||
}
|
||
addChild(path, node) {
|
||
(0, assert_1.assert)(!this.children[path], `无法定义相同路径[${path}]下的子节点`);
|
||
this.children[path] = node;
|
||
this.passRsToChild(path);
|
||
}
|
||
removeChild(path) {
|
||
(0, lodash_1.unset)(this.children, path);
|
||
}
|
||
getFreshValue() {
|
||
const inModiNextBranch = this.isInModiNextBranch();
|
||
const projection = this.getProjection(false);
|
||
const filter = this.getFilter(undefined, true);
|
||
if (projection && filter) {
|
||
/**
|
||
* 这里在非modi状态下,原来的代码是不会去刷新缺失的数据,原因不明,可能是认为页面应当自己负责数据的获取
|
||
* 在modi状态下,有些外键指向的数据无法预先获取,因此需要加上这个逻辑
|
||
*
|
||
* 先放回来,不知道有什么问题
|
||
* by Xc 20240229
|
||
*/
|
||
const id = this.getId();
|
||
const result = this.cache.get(this.entity, {
|
||
data: projection,
|
||
filter,
|
||
}, inModiNextBranch ? undefined : {
|
||
[id]: this.sr,
|
||
});
|
||
return result[0];
|
||
}
|
||
}
|
||
// 当node的id重置时,其一对多的儿子结点都应当刷新数据(条件已经改变)
|
||
refreshListChildren() {
|
||
for (const k in this.children) {
|
||
const child = this.children[k];
|
||
if (child instanceof ListNode) {
|
||
child.refresh(0, false);
|
||
}
|
||
}
|
||
}
|
||
create(lsn, data) {
|
||
const id = (0, uuid_1.generateNewId)();
|
||
(0, assert_1.assert)(!this.id && !this.dirty, 'create前要保证singleNode为空');
|
||
// 如果数据键值是一个空字符串则更新成null
|
||
for (const k in data) {
|
||
if (data[k] === '') {
|
||
Object.assign(data, {
|
||
[k]: null,
|
||
});
|
||
}
|
||
}
|
||
const now = Date.now();
|
||
this.ulManager.push(lsn, {
|
||
action: 'create',
|
||
data: Object.assign({}, data, {
|
||
id,
|
||
[types_1.CreateAtAttribute]: now,
|
||
[types_1.UpdateAtAttribute]: now,
|
||
}),
|
||
});
|
||
this.refreshListChildren();
|
||
this.setDirty();
|
||
}
|
||
update(lsn, data, action) {
|
||
// 如果数据键值是一个空字符串则更新成null,如果是undefined则取消此键值
|
||
const undefinedAttrs = [];
|
||
for (const k in data) {
|
||
if (data[k] === '' && !this.schema[this.entity].attributes[k].notNull) {
|
||
Object.assign(data, {
|
||
[k]: null,
|
||
});
|
||
}
|
||
else if (data[k] === undefined) {
|
||
undefinedAttrs.push(k);
|
||
}
|
||
}
|
||
undefinedAttrs.forEach((attr) => (0, lodash_1.unset)(data, attr));
|
||
this.ulManager.push(lsn, {
|
||
action: action || 'update',
|
||
data: Object.assign({}, data, {
|
||
[types_1.UpdateAtAttribute]: Date.now(),
|
||
}),
|
||
filter: {
|
||
id: this.getId(),
|
||
}
|
||
});
|
||
// 处理外键,如果update的数据中有相应的外键,其子对象上的动作应当被clean掉
|
||
// 并将sr传递到子组件上
|
||
// 这里可能有问题,还是应该由页面上显式传入对象主键保险。by Xc 20240623
|
||
/* for (const attr in data!) {
|
||
if (attr === 'entityId') {
|
||
assert(data.entity, '设置entityId时请将entity也传入');
|
||
if (this.children[data.entity]) {
|
||
this.children[data.entity].clean(true);
|
||
this.passRsToChild(data.entity);
|
||
}
|
||
}
|
||
else if (this.schema[this.entity]!.attributes[attr as any]?.type === 'ref') {
|
||
const refKey = attr.slice(0, attr.length - 2);
|
||
if (this.children[refKey]) {
|
||
this.children[refKey].clean(true);
|
||
this.passRsToChild(refKey);
|
||
}
|
||
}
|
||
} */
|
||
this.setDirty();
|
||
}
|
||
remove(lsn) {
|
||
const id = this.getId();
|
||
(0, assert_1.assert)(id, '当前节点的remove必须提供id');
|
||
this.ulManager.push(lsn, {
|
||
action: 'remove',
|
||
data: {
|
||
[types_1.DeleteAtAttribute]: Date.now(),
|
||
},
|
||
filter: {
|
||
id,
|
||
}
|
||
});
|
||
this.setDirty();
|
||
}
|
||
setDirty() {
|
||
const id = this.getId();
|
||
(0, assert_1.assert)(id);
|
||
this.ulManager.push(this.ulManager.maxLsn, {
|
||
action: 'update',
|
||
data: {},
|
||
filter: {
|
||
id,
|
||
}
|
||
});
|
||
super.setDirty();
|
||
}
|
||
/**
|
||
*
|
||
* @param path 如果指定了某条路径,则只处理这条路径上的operation(区分modi的前后项)
|
||
* @returns
|
||
*/
|
||
composeOperations(paths) {
|
||
if (this.dirty) {
|
||
const lsnMax = this.ulManager.maxLsn;
|
||
const path = paths?.shift();
|
||
for (const ele in this.children) {
|
||
if (path && path !== ele) {
|
||
continue;
|
||
}
|
||
const ele2 = ele.includes(':') ? ele.slice(0, ele.indexOf(':')) : ele;
|
||
const child = this.children[ele];
|
||
const childOperations = child.composeOperations(paths?.length ? paths : undefined);
|
||
if (childOperations) {
|
||
if (child instanceof SingleNode) {
|
||
(0, assert_1.assert)(childOperations.length === 1);
|
||
this.ulManager.push(lsnMax + 100, {
|
||
action: 'update',
|
||
data: {
|
||
[ele2]: childOperations[0].operation,
|
||
},
|
||
filter: {
|
||
id: this.getId(),
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(child instanceof ListNode);
|
||
this.ulManager.push(lsnMax + 100, {
|
||
action: 'update',
|
||
data: {
|
||
[ele2]: childOperations.map(({ operation }) => operation),
|
||
},
|
||
filter: {
|
||
id: this.getId(),
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
const operations = this.ulManager.makeOperations();
|
||
this.ulManager.rollback(lsnMax);
|
||
(0, assert_1.assert)(operations.length <= 1);
|
||
const [operation] = operations;
|
||
if (operation) {
|
||
const { filter, ...rest } = operation;
|
||
const realFilter = this.getFilter(true);
|
||
return [
|
||
{
|
||
entity: this.entity,
|
||
operation: {
|
||
...rest,
|
||
filter: realFilter,
|
||
},
|
||
}
|
||
];
|
||
}
|
||
}
|
||
}
|
||
getProjection(withDecendants) {
|
||
const projection = super.getProjection();
|
||
if (projection && withDecendants) {
|
||
for (const k in this.children) {
|
||
if (projection[k] && process.env.NODE_ENV === 'development') {
|
||
console.warn(`父结点都定义了${k}路径上的projection,和子结点产生冲突`);
|
||
}
|
||
if (!k.includes(exports.MODI_NEXT_PATH_SUFFIX)) {
|
||
const rel = this.judgeRelation(k.includes(':') ? k.split(':')[0] : k);
|
||
if (rel === 2) {
|
||
const subProjection = this.children[k].getProjection(true);
|
||
Object.assign(projection, {
|
||
entity: 1,
|
||
entityId: 1,
|
||
[k]: subProjection,
|
||
});
|
||
}
|
||
else if (typeof rel === 'string') {
|
||
const subProjection = this.children[k].getProjection(true);
|
||
Object.assign(projection, {
|
||
[`${k}Id`]: 1,
|
||
[k]: subProjection,
|
||
});
|
||
}
|
||
else if (!k.endsWith('$$aggr')) {
|
||
const child = this.children[k];
|
||
(0, assert_1.assert)(rel instanceof Array && child instanceof ListNode);
|
||
const subSelection = child.constructSelection();
|
||
const subEntity = child.getEntity();
|
||
Object.assign(projection, {
|
||
[k]: Object.assign(subSelection, {
|
||
$entity: subEntity,
|
||
})
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return projection;
|
||
}
|
||
passRsToChild(k) {
|
||
/**
|
||
* 把返回的结果中的total和aggr相关的值下降到相关的子结点上去
|
||
*/
|
||
const projection = this.getProjection(true);
|
||
const [value] = this.cache.get(this.entity, {
|
||
data: projection,
|
||
filter: {
|
||
id: this.id,
|
||
},
|
||
}, this.sr);
|
||
const keys = k ? [k] : Object.keys(this.children || {});
|
||
for (const k of keys) {
|
||
const child = this.children[k];
|
||
const k2 = k.split(':')[0]; // 处理:next
|
||
const rel = this.judgeRelation(k2);
|
||
if (rel === 2) {
|
||
if (value?.entityId) {
|
||
(0, assert_1.assert)(child instanceof SingleNode);
|
||
(0, assert_1.assert)(value.entity === child.getEntity());
|
||
child.saveRefreshResult({
|
||
[value.entityId]: this.sr[k2] || {},
|
||
});
|
||
}
|
||
else if (value && value.entityId === undefined && process.env.NODE_ENV === 'development') {
|
||
console.warn(`singleNode 的子路径「${k2}」上没有找到相应的entityId值,可能是取数据不全,请检查`);
|
||
}
|
||
}
|
||
else if (typeof rel === 'string') {
|
||
if (value && value[`${k2}Id`]) {
|
||
(0, assert_1.assert)(child instanceof SingleNode);
|
||
(0, assert_1.assert)(rel === child.getEntity());
|
||
child.saveRefreshResult({
|
||
[value[`${k2}Id`]]: this.sr[k2] || {},
|
||
});
|
||
}
|
||
else if (value && value[`${k2}Id`] === undefined && process.env.NODE_ENV === 'development') {
|
||
console.warn(`singleNode 的子路径「${k2}」上没有找到相应的${k2}Id值,可能是取数据不全,请检查`);
|
||
}
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(rel instanceof Array);
|
||
(0, assert_1.assert)(child instanceof ListNode);
|
||
// assert(this.sr![k]);
|
||
if (this.sr[k2]) {
|
||
child.saveRefreshResult(this.sr[k2]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
saveRefreshResult(data) {
|
||
const ids = Object.keys(data);
|
||
if (ids.length === 0) {
|
||
(0, assert_1.assert)(!this.id, '通过id查询数据结果为空');
|
||
}
|
||
// 这个情况不应该出现,查询数据的时候有判断,这里不可能出现>1的情况
|
||
(0, assert_1.assert)(ids.length === 1, '通过id查询数据结果不唯一');
|
||
this.setId(ids[0]);
|
||
this.sr = data[ids[0]];
|
||
this.passRsToChild();
|
||
// this.publish();
|
||
}
|
||
async refresh() {
|
||
// SingleNode如果是非根结点,其id应该在第一次refresh的时候来确定
|
||
const projection = this.getProjection(true);
|
||
const filter = this.getFilter(true);
|
||
if (projection && filter) {
|
||
const lr = this.startLoading();
|
||
if (lr instanceof Promise) {
|
||
return lr;
|
||
}
|
||
this.publish();
|
||
try {
|
||
await this.cache.refresh(this.entity, {
|
||
data: projection,
|
||
filter,
|
||
}, undefined, (result) => {
|
||
// 刷新后所有的更新都应当被丢弃(子层上可能会自动建立了this.create动作) 这里可能会有问题 by Xc 20230329
|
||
this.saveRefreshResult(result.data);
|
||
this.setFiltersAndSortedApplied();
|
||
this.endRefreshing(lr);
|
||
//this.clean();
|
||
});
|
||
}
|
||
catch (err) {
|
||
this.endRefreshing(lr, err);
|
||
this.publish();
|
||
throw err;
|
||
}
|
||
}
|
||
else {
|
||
// 不刷新也publish一下,触发页面reRender,不然有可能导致页面不进入formData
|
||
this.publish();
|
||
}
|
||
}
|
||
clean(lsn, dontPublish) {
|
||
for (const child in this.children) {
|
||
this.children[child].clean(lsn, true);
|
||
}
|
||
if (this.dirty) {
|
||
this.ulManager.rollback(lsn || 0);
|
||
if (!lsn) {
|
||
this.dirty = undefined;
|
||
}
|
||
if (!dontPublish) {
|
||
this.publish();
|
||
}
|
||
}
|
||
}
|
||
getFilter(ignoreNew, onlyHot) {
|
||
// 如果是新建,等于没有filter
|
||
const [operation] = this.ulManager.makeOperations();
|
||
if (operation?.action === 'create') {
|
||
if (ignoreNew) {
|
||
return;
|
||
}
|
||
const { id } = operation.data;
|
||
return { id };
|
||
}
|
||
// singleNode增加一些限定的filter可以优化后台权限的判断范围和一些trigger的条件
|
||
// 如果没有this.id则不返回,避免一些奇怪的边界(比如execute以后refresh)
|
||
if (this.id) {
|
||
let filter = {
|
||
id: this.id,
|
||
};
|
||
if (this.filters) {
|
||
filter = (0, filter_1.combineFilters)(this.entity, this.schema, this.filters.filter(ele => !onlyHot || ele.hot).map(ele => typeof ele.filter === 'function' ? ele.filter() : ele.filter).concat(filter));
|
||
}
|
||
if (this.parent && this.parent instanceof ListNode && this.parent.getEntity() === this.entity) {
|
||
const { filter: parentFilter } = this.parent.constructSelection(true, true, true);
|
||
filter = (0, filter_1.combineFilters)(this.entity, this.schema, [filter, parentFilter]);
|
||
}
|
||
return filter;
|
||
}
|
||
}
|
||
getIntrinsticFilters() {
|
||
return this.getFilter();
|
||
}
|
||
/**
|
||
* getParentFilter不能假设一定已经有数据,只能根据当前filter的条件去构造
|
||
* @param childNode
|
||
* @param disableOperation
|
||
* @returns
|
||
*/
|
||
getParentFilter(childNode, ignoreNewParent) {
|
||
const value = this.getFreshValue();
|
||
if (!value || (value && value.$$createAt$$ === 1 && ignoreNewParent)) {
|
||
return;
|
||
}
|
||
for (const key in this.children) {
|
||
if (childNode === this.children[key]) {
|
||
const rel = this.judgeRelation(key);
|
||
(0, assert_1.assert)(rel instanceof Array && !key.endsWith('$$aggr'));
|
||
if (rel[1]) {
|
||
// 基于普通外键的一对多
|
||
if (value) {
|
||
return {
|
||
[rel[1]]: value.id,
|
||
};
|
||
}
|
||
const filter = this.getFilter();
|
||
if (filter) {
|
||
if (filter.id && Object.keys(filter).length === 1) {
|
||
return {
|
||
[rel[1]]: filter.id,
|
||
};
|
||
}
|
||
return {
|
||
[rel[1].slice(0, rel[1].length - 2)]: filter,
|
||
};
|
||
}
|
||
}
|
||
else {
|
||
// 基于entity/entityId的一对多
|
||
if (value) {
|
||
return {
|
||
entity: this.entity,
|
||
entityId: value.id,
|
||
};
|
||
}
|
||
const filter = this.getFilter();
|
||
if (filter) {
|
||
if (filter.id && Object.keys(filter).length === 1) {
|
||
return {
|
||
entity: this.entity,
|
||
entityId: filter.id,
|
||
};
|
||
}
|
||
return {
|
||
[this.entity]: filter,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
class VirtualNode extends Node {
|
||
parent;
|
||
refreshing2 = false;
|
||
children;
|
||
constructor(fullPath, path, parent, stale) {
|
||
super(fullPath);
|
||
this.children = {};
|
||
this.stale = stale;
|
||
if (parent) {
|
||
parent.addChild(path, this);
|
||
this.parent = parent;
|
||
}
|
||
}
|
||
getModiOperations() {
|
||
for (const c in this.children) {
|
||
const child = this.children[c];
|
||
if (child instanceof SingleNode) {
|
||
const result = child.getModiOperations();
|
||
if (result) {
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
setDirty() {
|
||
this.dirty = true;
|
||
this.publish();
|
||
}
|
||
setFiltersAndSortedApplied() {
|
||
for (const k in this.children) {
|
||
this.children[k].setFiltersAndSortedApplied();
|
||
}
|
||
}
|
||
addChild(path, child) {
|
||
// 规范virtualNode子结点的命名路径和类型,entity的singleNode必须被命名为entity或entity:number,ListNode必须被命名为entitys或entitys:number
|
||
(0, assert_1.assert)(!this.children[path], `无法定义相同路径[${path}]下的子节点`);
|
||
this.children[path] = child;
|
||
if (child instanceof SingleNode || child instanceof ListNode) {
|
||
const entity = child.getEntity();
|
||
if (child instanceof SingleNode) {
|
||
(0, assert_1.assert)(path === entity || path.startsWith(`${entity}:`), `oakPath「${path}」不符合命名规范,请以「${entity}:」为命名起始标识`);
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(path === `${entity}s` || path.startsWith(`${entity}s:`), `oakPath「${path}」不符合命名规范,请以「${entity}s:」为命名起始标识`);
|
||
}
|
||
}
|
||
}
|
||
removeChild(path) {
|
||
(0, lodash_1.unset)(this.children, path);
|
||
}
|
||
getChild(path) {
|
||
return this.children[path];
|
||
}
|
||
getParent() {
|
||
return this.parent;
|
||
}
|
||
destroy() {
|
||
for (const k in this.children) {
|
||
this.children[k].destroy();
|
||
}
|
||
}
|
||
getFreshValue() {
|
||
return undefined;
|
||
}
|
||
isLoading() {
|
||
return this.refreshing2;
|
||
}
|
||
async refresh() {
|
||
this.refreshing2 = true;
|
||
const promises = Object.keys(this.children).map(ele => this.children[ele].refresh());
|
||
try {
|
||
this.publish();
|
||
await Promise.all(promises);
|
||
this.refreshing2 = false;
|
||
this.publish();
|
||
}
|
||
catch (err) {
|
||
this.refreshing2 = false;
|
||
this.publish();
|
||
throw err;
|
||
}
|
||
}
|
||
composeOperations(paths) {
|
||
/**
|
||
* 当一个virtualNode有多个子结点,而这些子结点的前缀一致时,标识这些子结点其实是指向同一个对象,此时需要合并
|
||
*/
|
||
const operationss = [];
|
||
const operationDict = {};
|
||
const path = paths?.shift();
|
||
for (const ele in this.children) {
|
||
if (path && path !== ele) {
|
||
continue;
|
||
}
|
||
const operation = this.children[ele].composeOperations(paths?.length ? paths : undefined);
|
||
if (operation) {
|
||
const idx = ele.indexOf(':') !== -1 ? ele.slice(0, ele.indexOf(':')) : ele;
|
||
if (operationDict[idx]) {
|
||
(0, assert_1.assert)(false, '这种情况不应当再出现');
|
||
// 需要合并这两个子结点的动作 todo 两个子结点指向同一个对象,这种情况应当要消除
|
||
/* if (this.children[ele] instanceof SingleNode) {
|
||
// mergeOperationOper(this.children[ele].getEntity(), this.children[ele].getSchema(), operation[0], operationDict[idx][0]);
|
||
}
|
||
else {
|
||
console.warn('发生virtualNode上的list页面的动作merge,请查看');
|
||
operationDict[idx].push(...operation);
|
||
} */
|
||
}
|
||
else {
|
||
operationDict[idx] = operation;
|
||
}
|
||
}
|
||
}
|
||
for (const k in operationDict) {
|
||
operationss.push(...operationDict[k]);
|
||
}
|
||
return operationss;
|
||
}
|
||
clean(lsn, dontPublish) {
|
||
for (const ele in this.children) {
|
||
this.children[ele].clean(lsn, true);
|
||
}
|
||
if (!lsn) {
|
||
this.dirty = false;
|
||
}
|
||
if (!dontPublish) {
|
||
this.publish();
|
||
}
|
||
}
|
||
checkIfClean(node) {
|
||
for (const k in this.children) {
|
||
if (this.children[k] === node) {
|
||
continue;
|
||
}
|
||
if (this.children[k].isDirty()) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
function analyzePath(path) {
|
||
const paths = path.split('.');
|
||
const length = paths.length;
|
||
return {
|
||
root: paths[0],
|
||
parent: length > 1 ? paths.slice(0, length - 1).join('.') : '',
|
||
path: paths[length - 1],
|
||
};
|
||
}
|
||
class RunningTree extends Feature_1.Feature {
|
||
logSerailNumber;
|
||
cache;
|
||
schema;
|
||
root;
|
||
constructor(cache, schema) {
|
||
super();
|
||
// this.aspectWrapper = aspectWrapper;
|
||
this.cache = cache;
|
||
this.schema = schema;
|
||
this.root = {};
|
||
this.logSerailNumber = START_LSN;
|
||
this.cache.bindOnSync((opRecords) => {
|
||
opRecords.forEach((record) => {
|
||
const { a } = record;
|
||
switch (a) {
|
||
case 'c': {
|
||
const { e } = record;
|
||
if (e === 'modi') {
|
||
this.invalidateCachedModis();
|
||
}
|
||
break;
|
||
}
|
||
case 'r': {
|
||
const { e } = record;
|
||
if (e === 'modi') {
|
||
this.invalidateCachedModis();
|
||
}
|
||
break;
|
||
}
|
||
case 's': {
|
||
const { d } = record;
|
||
for (const e in d) {
|
||
if (e === 'modi') {
|
||
this.invalidateCachedModis();
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
default: {
|
||
const { e } = record;
|
||
if (e === 'modi') {
|
||
this.invalidateCachedModis();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
createNode(options, isPage) {
|
||
const { entity, pagination, path: fullPath, filters, sorters, projection, isList, id, getTotal, zombie, stale, } = options;
|
||
let node;
|
||
const { parent, path } = analyzePath(fullPath);
|
||
const parentNode = parent ? this.findNode(parent) : undefined;
|
||
node = this.findNode(fullPath);
|
||
// 找到了之前的node,需要检查是否需要重用
|
||
if (node) {
|
||
if (node.getCount()) { // 有组件正在使用
|
||
// 页面组件已经被销毁,现在重新挂载,提示需要zombie
|
||
(0, assert_1.assert)(!isPage || zombie, '暂时在路由上可能回归的页面必须设置为zombie'); // 是页面并且不是zombie
|
||
// 这次非virtualNode
|
||
if (entity) {
|
||
if (node instanceof ListNode) {
|
||
if (isList && node.getEntity() === entity) {
|
||
// 如果之前未初始化,当前组件负责初始化
|
||
if (!node.getProjection() && projection) {
|
||
node.setProjection(projection);
|
||
if (filters) {
|
||
node.setNamedFilters(filters);
|
||
}
|
||
if (sorters) {
|
||
node.setNamedSorters(sorters);
|
||
}
|
||
if (pagination) {
|
||
node.setPagination(pagination, true); // 创建成功后会统一refresh
|
||
}
|
||
}
|
||
else if (projection) {
|
||
// 这里有一个例外是queryPanel这种和父结点共用此结点的抽象组件
|
||
// assert(false, `创建node时发现path[${fullPath}]已经存在有效的ListNod结点,这种情况不应该存在`);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(node instanceof SingleNode, `节点[${path}]应为Virtual节点,但现在是[${entity.toString()}]请检查oakPath是否重复`);
|
||
(0, assert_1.assert)(!isList && node.getEntity() === entity, `节点${path}的entity为${node.getEntity().toString()},但现在为${entity.toString()},请检查oakPath是否重复`);
|
||
if (!node.getProjection() && projection) {
|
||
node.setProjection(projection);
|
||
if (id) {
|
||
const id2 = node.getId();
|
||
(0, assert_1.assert)(id === id2, `重用path[${fullPath}]上的singleNode时,其上没有置有效id,这种情况id应当由父结点设置`);
|
||
}
|
||
}
|
||
else {
|
||
// 目前只有一种情况合法,即parentNode是list,列表中的位置移动引起的重用
|
||
// assert(parentNode instanceof ListNode, `创建node时发现path[${fullPath}]已有有效的SingleNode结点,本情况不应当存在`);
|
||
}
|
||
}
|
||
}
|
||
// 上次不是virtualNode,这次变成virtual了
|
||
else {
|
||
(0, assert_1.assert)(false, `创建Virtual节点时发现path[${fullPath}]已经存在有效的结点,请检查oakPath是否重复`);
|
||
// assert(false, '这种情况暂时不考虑,跑出来再处理,by Xc 20240717');
|
||
}
|
||
}
|
||
else {
|
||
// 这种情况是重用zombie留下的node,啥也别做
|
||
if (node instanceof SingleNode || node instanceof ListNode) {
|
||
(0, assert_1.assert)(node.getEntity() === entity, 'zombie留下的node, entity不能变化');
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
const rollback = this.begin();
|
||
if (parentNode) {
|
||
this.redoBranchModis(fullPath);
|
||
this.redoBranchOperations(fullPath);
|
||
}
|
||
if (entity) {
|
||
if (isList) {
|
||
(0, assert_1.assert)(!(parentNode instanceof ListNode));
|
||
// assert(projection, `页面没有定义投影「${path}」`);
|
||
node = new ListNode(entity, this.schema, this.cache, fullPath, projection, parentNode, path, filters, sorters, getTotal, pagination, stale);
|
||
}
|
||
else {
|
||
node = new SingleNode(entity, this.schema, this.cache, fullPath, path, projection, parentNode, // 过编译
|
||
id, filters, stale);
|
||
}
|
||
}
|
||
else {
|
||
(0, assert_1.assert)(!parentNode || parentNode instanceof VirtualNode);
|
||
node = new VirtualNode(fullPath, path, parentNode, stale);
|
||
}
|
||
rollback();
|
||
if (!parentNode) {
|
||
(0, assert_1.assert)(!parent && !this.root[path]);
|
||
this.root[path] = node;
|
||
}
|
||
node.subscribe(() => {
|
||
this.publish(fullPath);
|
||
});
|
||
}
|
||
node.increaseCount();
|
||
if (zombie) {
|
||
// 像Pagination, FilterPanel, ExtrafileCommit这种要和某个Component共用路径的,按要求都应该是后创建,不能影响原来的zombie
|
||
node.setZombie(zombie);
|
||
}
|
||
if (node instanceof SingleNode && !node.getId()) {
|
||
node.create(this.logSerailNumber, {});
|
||
this.invalidateCachedOperations(fullPath);
|
||
}
|
||
return node;
|
||
}
|
||
findNode(path) {
|
||
if (this.root[path]) {
|
||
return this.root[path];
|
||
}
|
||
const paths = path.split('.');
|
||
let node = this.root[paths[0]];
|
||
let iter = 1;
|
||
while (iter < paths.length && node) {
|
||
if (!node) {
|
||
return;
|
||
}
|
||
const childPath = paths[iter];
|
||
iter++;
|
||
node = node.getChild(childPath);
|
||
}
|
||
return node;
|
||
}
|
||
destroyNode(path, isPage) {
|
||
const node = this.findNode(path);
|
||
// 如果node不存在,说明父结点已经析构掉了,无视即可
|
||
if (!node) {
|
||
return;
|
||
}
|
||
const cnt = node.decreaseCount();
|
||
if (cnt !== 0) {
|
||
return;
|
||
}
|
||
if (node.isZombie()) {
|
||
return;
|
||
}
|
||
// 不是声明为zombie结点就析构
|
||
const childPath = path.slice(path.lastIndexOf('.') + 1);
|
||
const parent = node.getParent();
|
||
if (parent) {
|
||
parent.removeChild(childPath);
|
||
}
|
||
else if (!parent) {
|
||
(0, assert_1.assert)(this.root.hasOwnProperty(path));
|
||
(0, lodash_1.unset)(this.root, path);
|
||
}
|
||
this.invalidateCachedOperations(path);
|
||
node.destroy();
|
||
node.clearSubscribes();
|
||
}
|
||
begin() {
|
||
return this.cache.begin();
|
||
}
|
||
getFreshValue(path) {
|
||
const node = this.findNode(path);
|
||
if (node instanceof ListNode) {
|
||
return node.getFreshValue();
|
||
}
|
||
else if (node instanceof SingleNode) {
|
||
return node.getFreshValue();
|
||
}
|
||
}
|
||
isDirty(path) {
|
||
const node = this.findNode(path);
|
||
return node ? node.isDirty() : false;
|
||
}
|
||
addItem(path, data) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用addItem');
|
||
return node.addItem(this.logSerailNumber, data);
|
||
}
|
||
addItems(path, data) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用addItems');
|
||
return node.addItems(this.logSerailNumber, data);
|
||
}
|
||
removeItem(path, id) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用removeItem');
|
||
node.removeItem(this.logSerailNumber, id);
|
||
}
|
||
removeItems(path, ids) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用removeItems');
|
||
node.removeItems(this.logSerailNumber, ids);
|
||
}
|
||
updateItem(path, data, id, action) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用updateItem');
|
||
node.updateItem(this.logSerailNumber, data, id, action);
|
||
}
|
||
updateItems(path, data, ids, action) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用updateItems');
|
||
node.updateItems(this.logSerailNumber, data, ids, action);
|
||
}
|
||
recoverItem(path, id) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用recoverItem');
|
||
node.recoverItem(id);
|
||
}
|
||
recoverItems(path, ids) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用recoverItems');
|
||
node.recoverItems(ids);
|
||
}
|
||
resetItem(path, id) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用resetItem');
|
||
node.resetItem(id);
|
||
}
|
||
create(path, data) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof SingleNode, '只能在SingleNode上调用create');
|
||
node.create(this.logSerailNumber, data);
|
||
}
|
||
update(path, data, action) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof SingleNode, '只能在SingleNode上调用update');
|
||
node.update(this.logSerailNumber, data, action);
|
||
}
|
||
remove(path) {
|
||
this.invalidateCachedOperations(path);
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof SingleNode, '只能在SingleNode上调用remove');
|
||
node.remove(this.logSerailNumber);
|
||
}
|
||
isLoading(path) {
|
||
const node = this.findNode(path);
|
||
return node?.isLoading();
|
||
}
|
||
isLoadingMore(path) {
|
||
const node = this.findNode(path);
|
||
if (node instanceof ListNode) {
|
||
return node?.isLoadingMore();
|
||
}
|
||
return false;
|
||
}
|
||
isExecuting(path) {
|
||
const node = this.findNode(path);
|
||
return node ? node.isExecuting() : false;
|
||
}
|
||
isListChildOrStale(path) {
|
||
const node = this.findNode(path);
|
||
if (node?.isStale()) {
|
||
return true;
|
||
}
|
||
let parent = node?.getParent();
|
||
if (parent instanceof ListNode) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
// 判断当前结点是否在:next的路径上
|
||
// 当前判断规则是:在:next的分岔结点上,如果有前项结点,或者:next结点本身是一个create,则不再刷新
|
||
isInModiNextBranch(path) {
|
||
if (!path.includes(exports.MODI_NEXT_PATH_SUFFIX)) {
|
||
return false;
|
||
}
|
||
const paths = path.split('.');
|
||
if (paths[0].includes(exports.MODI_NEXT_PATH_SUFFIX)) {
|
||
// 根结点就是:next,这时候还是要取的
|
||
return false;
|
||
}
|
||
let iter = 1;
|
||
while (iter < paths.length) {
|
||
if (paths[iter].includes(exports.MODI_NEXT_PATH_SUFFIX)) {
|
||
const pathPrev = paths.slice(0, iter).join('.') + '.' + paths[iter].replace(exports.MODI_NEXT_PATH_SUFFIX, '');
|
||
const node = this.findNode(pathPrev);
|
||
if (node) {
|
||
return true;
|
||
}
|
||
// 还有一种情况,这个:next结点是创建出来的
|
||
const pathNext = paths.slice(0, iter + 1).join('.');
|
||
const nodeNext = this.findNode(pathNext);
|
||
if (nodeNext instanceof SingleNode) {
|
||
const id = nodeNext.getId();
|
||
const [modi] = this.cache.get('modi', {
|
||
data: {
|
||
id: 1,
|
||
},
|
||
filter: {
|
||
action: 'create',
|
||
targetEntity: nodeNext.getEntity(),
|
||
filter: {
|
||
id,
|
||
},
|
||
}
|
||
});
|
||
if (modi) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
iter++;
|
||
}
|
||
(0, assert_1.assert)(false);
|
||
return false;
|
||
}
|
||
async refresh(path, pageNumber, resetTotal) {
|
||
if (this.isInModiNextBranch(path)) {
|
||
this.publish(path);
|
||
return;
|
||
}
|
||
const node = this.findNode(path);
|
||
if (node instanceof ListNode) {
|
||
await node.refresh(pageNumber, false, resetTotal);
|
||
}
|
||
else if (node) {
|
||
await node.refresh();
|
||
}
|
||
}
|
||
async loadMore(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode);
|
||
await node.loadMore();
|
||
}
|
||
getPagination(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用getPagination');
|
||
const pn = node.getPagination();
|
||
return {
|
||
...pn,
|
||
currentPage: pn.currentPage + 1,
|
||
};
|
||
}
|
||
setId(path, id) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof SingleNode, '只能在SingleNode上调用setId');
|
||
return node.setId(id);
|
||
}
|
||
unsetId(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof SingleNode, '只能在SingleNode上调用unsetId');
|
||
node.unsetId();
|
||
}
|
||
getId(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof SingleNode, '只能在SingleNode上调用getId');
|
||
return node.getId();
|
||
}
|
||
getEntity(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof EntityNode, '只能在EntityNode上调用getEntity');
|
||
return node.getEntity();
|
||
}
|
||
setPageSize(path, pageSize) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用setPageSize');
|
||
// 切换分页pageSize就重新设置
|
||
return node.setPagination({
|
||
pageSize,
|
||
currentPage: 0,
|
||
total: undefined,
|
||
});
|
||
}
|
||
setCurrentPage(path, currentPage) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用setCurrentPage');
|
||
return node.setCurrentPage(currentPage - 1);
|
||
}
|
||
getNamedFilters(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用getNamedFilters');
|
||
return node.getNamedFilters();
|
||
}
|
||
getNamedFilterByName(path, name) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用getNamedFilterByName');
|
||
return node.getNamedFilterByName(name);
|
||
}
|
||
setNamedFilters(path, filters, refresh = true) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用setNamedFilters');
|
||
node.setNamedFilters(filters, refresh);
|
||
}
|
||
addNamedFilter(path, filter, refresh = false) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用addNamedFilter');
|
||
return node.addNamedFilter(filter, refresh);
|
||
}
|
||
removeNamedFilter(path, filter, refresh = false) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用removeNamedFilter');
|
||
return node.removeNamedFilter(filter, refresh);
|
||
}
|
||
removeNamedFilterByName(path, name, refresh = false) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用removeNamedFilterByName');
|
||
return node.removeNamedFilterByName(name, refresh);
|
||
}
|
||
getNamedSorters(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用getNamedSorters');
|
||
return node.getNamedSorters();
|
||
}
|
||
getNamedSorterByName(path, name) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用getNamedSorterByName');
|
||
return node.getNamedSorterByName(name);
|
||
}
|
||
setNamedSorters(path, sorters, refresh = true) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用setNamedSorters');
|
||
return node.setNamedSorters(sorters, refresh);
|
||
}
|
||
addNamedSorter(path, sorter, refresh = false) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用addNamedSorter');
|
||
return node.addNamedSorter(sorter, refresh);
|
||
}
|
||
removeNamedSorter(path, sorter, refresh = false) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用removeNamedSorter');
|
||
return node.removeNamedSorter(sorter, refresh);
|
||
}
|
||
removeNamedSorterByName(path, name, refresh = false) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode, '只能在ListNode上调用removeNamedSorterByName');
|
||
return node.removeNamedSorterByName(name, refresh);
|
||
}
|
||
getIntrinsticFilters(path) {
|
||
const node = this.findNode(path);
|
||
(0, assert_1.assert)(node instanceof ListNode || node instanceof SingleNode);
|
||
return node.getIntrinsticFilters();
|
||
}
|
||
getOperations(path) {
|
||
const node = this.findNode(path);
|
||
const operations = node?.composeOperations();
|
||
return operations;
|
||
}
|
||
cachedOperations = {};
|
||
cachedModis = {};
|
||
invalidateCachedOperations(path) {
|
||
const invalidOnes = [];
|
||
for (const k in this.cachedOperations) {
|
||
if (path.startsWith(k) || k.startsWith(path)) {
|
||
invalidOnes.push(k);
|
||
}
|
||
}
|
||
invalidOnes.forEach((key) => (0, lodash_1.unset)(this.cachedOperations, key));
|
||
}
|
||
invalidateCachedModis() {
|
||
this.cachedModis = {};
|
||
}
|
||
/**
|
||
* 这个函数在reRender时可能会反复调用,composeOperations很消耗性能,在这里要做个缓存
|
||
* @param path
|
||
*/
|
||
redoBranchOperations(path) {
|
||
const paths = path.split('.');
|
||
const root = paths.shift();
|
||
const opers = this.cachedOperations[path] || this.root[root].composeOperations(paths);
|
||
if (opers) {
|
||
if (!this.cachedOperations[path]) {
|
||
this.cachedOperations[path] = opers;
|
||
}
|
||
this.cache.redoOperation(opers);
|
||
}
|
||
}
|
||
/**
|
||
* 这个函数在reRender时可能会反复调用,getModiOperations很消耗性能,在这里要做个缓存
|
||
* @param path
|
||
*/
|
||
redoBranchModis(path) {
|
||
const { root } = analyzePath(path);
|
||
const rootNode = this.root[root];
|
||
const includeModi = path.includes(exports.MODI_NEXT_PATH_SUFFIX);
|
||
if (includeModi) {
|
||
const modiOperations = this.cachedModis[root] || rootNode.getModiOperations();
|
||
if (!this.cachedModis[root]) {
|
||
this.cachedModis[root] = modiOperations;
|
||
}
|
||
modiOperations && this.cache.redoOperation(modiOperations);
|
||
}
|
||
}
|
||
async execute(path, action, opers) {
|
||
const node = path && this.findNode(path);
|
||
// assert(node.isDirty());
|
||
node && node.setExecuting(true);
|
||
let pollute = false;
|
||
try {
|
||
let operations = path && this.getOperations(path) || [];
|
||
if (opers) {
|
||
operations.push(...opers);
|
||
}
|
||
// 这里理论上virtualNode下面也可以有多个不同的entity的组件,但实际中不应当出现这样的设计
|
||
if (action) {
|
||
if (operations.length > 0) {
|
||
(0, assert_1.assert)(operations.length === 1);
|
||
const [operation1] = operations;
|
||
if (action !== operation1.operation.action) {
|
||
(0, assert_1.assert)(operation1.operation.action === 'update'); // 如果execute时传action,前面update动作应该只可能是update
|
||
operation1.operation.action = action;
|
||
}
|
||
}
|
||
else if (node) {
|
||
// 老的写法,直接对一个非脏的结点execute某个action,也可以支持
|
||
(0, assert_1.assert)(node instanceof SingleNode);
|
||
node.update(this.logSerailNumber, {}, action);
|
||
pollute = true;
|
||
operations = node.composeOperations() || [];
|
||
(0, assert_1.assert)(operations.length === 1);
|
||
const [operation1] = operations;
|
||
if (action !== operation1.operation.action) {
|
||
(0, assert_1.assert)(operation1.operation.action === 'update'); // 如果execute时传action,前面update动作应该只可能是update
|
||
operation1.operation.action = action;
|
||
}
|
||
}
|
||
}
|
||
if (operations.length > 0) {
|
||
const entities = (0, lodash_1.uniq)(operations.filter((ele) => !!ele).map((ele) => ele.entity));
|
||
(0, assert_1.assert)(entities.length === 1);
|
||
const result = await this.cache.operate(entities[0], operations
|
||
.filter((ele) => !!ele)
|
||
.map((ele) => ele.operation), undefined, () => {
|
||
// 清空缓存
|
||
path && this.clean(path, undefined, true);
|
||
if (node && node instanceof SingleNode) {
|
||
(0, assert_1.assert)(operations.length === 1);
|
||
// 这逻辑有点扯,页面自己决定后续逻辑 by Xc 20231108
|
||
// if (operations[0].operation.action === 'create') {
|
||
// // 如果是create动作,给结点赋上id,以保证页面数据的完整性
|
||
// const { id } = operations[0].operation.data as ED[keyof ED]['CreateSingle']['data'];
|
||
// node.setId(id);
|
||
// }
|
||
}
|
||
node && node.setExecuting(false);
|
||
});
|
||
return result;
|
||
}
|
||
path && this.clean(path, undefined, true);
|
||
node && node.setExecuting(false);
|
||
return { message: 'No Operation' };
|
||
}
|
||
catch (err) {
|
||
node && node.setExecuting(false);
|
||
if (pollute) {
|
||
path && this.clean(path);
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
savePoint() {
|
||
const lsn = this.logSerailNumber;
|
||
this.logSerailNumber++;
|
||
return lsn;
|
||
}
|
||
/**
|
||
* 将path上的更新清除
|
||
* @param path
|
||
* @param lsn 要清除到某个指定的savepoint,不设则完全清空
|
||
* @param dontPublish
|
||
*/
|
||
clean(path, lsn, dontPublish) {
|
||
let node = this.findNode(path);
|
||
const { root } = analyzePath(path);
|
||
this.invalidateCachedOperations(root);
|
||
while (true) {
|
||
const parent = node?.getParent();
|
||
if (parent && node && parent.checkIfClean(node)) {
|
||
node = parent;
|
||
continue;
|
||
}
|
||
else {
|
||
node?.clean(lsn, dontPublish);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
getRoot() {
|
||
return this.root;
|
||
}
|
||
saveExtraData(path, key, data) {
|
||
const node = this.findNode(path);
|
||
node.saveExtraData(key, data);
|
||
}
|
||
loadExtraData(path, key) {
|
||
const node = this.findNode(path);
|
||
return node.loadExtraData(key);
|
||
}
|
||
}
|
||
exports.RunningTree = RunningTree;
|