dehub
Version:
Data&Event MessageHub.
1,148 lines (1,147 loc) • 46.5 kB
JavaScript
"use strict";
/**
* DEData 模块
* 这是一个数据管理模块,负责数据的存储、加载、更新和状态管理
* @module DEData
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getData = exports.clearDataState = exports.clearTempDataState = exports.delFromDB = exports.getFromDB = exports.updateData = exports.saveToDB = exports.clearDatabase = exports.hasDataState = exports.getStore = exports.getStoreName = void 0;
const lodash_1 = require("lodash");
const DEHub_1 = require("./DEHub");
const DETypes_1 = require("./DETypes");
const idb_keyval_1 = require("idb-keyval");
/** 数据加载超时时间(毫秒) */
const LOADING_TIMEOUT = 15000;
/** 临时数据清理延迟时间(毫秒) */
const TEMP_DATA_CLEANUP_DELAY = 2000;
/** IndexedDB 读取超时时间(毫秒) */
const DB_READ_TIMEOUT = 5000;
/** 全局数据状态映射表 */
const dataStateMap = new Map();
/** 全局临时数据状态映射表 */
const dataTempStateMap = new Map();
const statusWaitersMap = new Map();
/**
* 获取存储名称
* @param database 数据库名称
* @returns [数据库名称, 客户端标识]
*/
const getStoreName = (database) => {
return [`db.${database ?? 'DEHub'}`, database || '$client'];
};
exports.getStoreName = getStoreName;
/**
* 获取存储实例
* @param database 数据库名称
* @returns 存储实例
*/
const getStore = (database) => {
const dbName = (0, exports.getStoreName)(database);
return (0, idb_keyval_1.createStore)(dbName[0], dbName[1]);
};
exports.getStore = getStore;
/**
* 检查是否存在数据状态
* @param tag 数据标签
* @returns 是否存在数据状态
*/
const hasDataState = (tag) => {
const itemMapPath = (tag instanceof DETypes_1.Tag ? tag : new DETypes_1.Tag(tag)).path;
return dataStateMap.has(itemMapPath);
};
exports.hasDataState = hasDataState;
/**
* 清空数据库
* @param database 数据库名称
*/
const clearDatabase = async (database) => {
const db = (0, exports.getStore)(database);
await (0, idb_keyval_1.clear)(db);
};
exports.clearDatabase = clearDatabase;
/**
* 保存数据到数据库
* @param tag 数据标签
* @param data 数据对象
* @param database 数据库名称
*/
const saveToDB = async (tag, data, database) => {
const store = (0, exports.getStore)(database);
const tTag = tag && tag instanceof DETypes_1.Tag ? tag : new DETypes_1.Tag(tag);
dataTempStateMap.set(tTag.path, data);
await (0, idb_keyval_1.set)(tTag.path, data, store);
};
exports.saveToDB = saveToDB;
const updateData = (tag, data, database) => {
const tTag = tag && tag instanceof DETypes_1.Tag ? tag : new DETypes_1.Tag(tag);
const targetData = (0, exports.getData)(tTag);
if (targetData) {
targetData.update(data, true);
}
else {
(0, exports.getFromDB)(tTag, database).then(storeData => {
if (!storeData) {
return;
}
storeData.data = data;
(0, exports.saveToDB)(tTag, storeData, database);
}).catch(err => {
console.error('[DEData] updateData failed:', err);
});
}
};
exports.updateData = updateData;
const getFromDB = async (tag, database) => {
return new Promise((resolve, reject) => {
const store = (0, exports.getStore)(database);
const wTimerId = setTimeout(() => {
// 超时后仅 reject,绝不自动删除用户数据
reject(new Error(`timeout: 获取标签为 ${tag.path} 的本地存储超时。`));
}, DB_READ_TIMEOUT);
(0, idb_keyval_1.get)(tag.path, store).then((value) => {
resolve(value);
}).catch((reason) => {
reject(reason);
}).finally(() => {
clearTimeout(wTimerId);
});
});
};
exports.getFromDB = getFromDB;
const delFromDB = async (tag, database) => {
const store = (0, exports.getStore)(database);
const tTag = tag && tag instanceof DETypes_1.Tag ? tag : new DETypes_1.Tag(tag);
dataTempStateMap.delete(tTag.path);
await (0, idb_keyval_1.del)(tTag.path, store);
};
exports.delFromDB = delFromDB;
const clearTempDataState = (tag) => {
const tTag = tag && tag instanceof DETypes_1.Tag ? tag : new DETypes_1.Tag(tag);
const objMapPath = tTag.path;
dataStateMap.delete(objMapPath);
dataTempStateMap.delete(objMapPath);
};
exports.clearTempDataState = clearTempDataState;
const clearDataState = async (database, tag) => {
try {
if ((!database || database === "*") && !tag) {
dataStateMap.clear();
dataTempStateMap.clear();
statusWaitersMap.clear();
if (database === "*") {
// 遍历所有已知数据库进行清理
const dbNames = new Set();
for (const data of dataStateMap.values()) {
dbNames.add(data.options.database);
}
dbNames.add(undefined); // 默认数据库
for (const dbName of dbNames) {
await (0, exports.clearDatabase)(dbName);
}
}
else {
const store = (0, exports.getStore)(database);
await (0, idb_keyval_1.clear)(store);
}
}
else if (tag) {
const tTag = tag && tag instanceof DETypes_1.Tag ? tag : new DETypes_1.Tag(tag);
const objMapPath = tTag.path;
dataStateMap.delete(objMapPath);
dataTempStateMap.delete(objMapPath);
statusWaitersMap.delete(objMapPath);
const store = (0, exports.getStore)(database);
await (0, idb_keyval_1.del)(tTag.path, store);
}
else {
const allDdbkeys = Array.from(dataStateMap.keys()).filter(k => k.startsWith(`${database ?? '$'}.`));
for (const k of allDdbkeys) {
dataStateMap.delete(k);
dataTempStateMap.delete(k);
statusWaitersMap.delete(k);
}
await (0, exports.clearDatabase)(database);
}
}
catch (err) {
throw err;
}
};
exports.clearDataState = clearDataState;
/**
* resolve目标状态映射
* 说明:键为当前状态,值为等待的兼容状态,即所有待等的兼容状态都可以认为目标达成
* 需要考虑的状态:Deleted,Ready,Failed,Timeout
*/
const resolveStatusMap = {
[DETypes_1.ObjectStatus.Ready]: [DETypes_1.ObjectStatus.Deleted],
[DETypes_1.ObjectStatus.Deleted]: [DETypes_1.ObjectStatus.Ready],
[DETypes_1.ObjectStatus.Failed]: [DETypes_1.ObjectStatus.Timeout]
};
const resolveStatus = (targetStatus) => {
const resStatus = [...(resolveStatusMap[targetStatus] ?? []), targetStatus];
return resStatus;
};
/**
* reject目标状态映射
* 说明:键为当前状态,值为相反状态,当到达当前状态时,值状态为reject.
* 需要考虑的状态:Deleted,Ready,Failed,Timeout
*/
const rejectStatusMap = {
[DETypes_1.ObjectStatus.Ready]: [DETypes_1.ObjectStatus.Failed, DETypes_1.ObjectStatus.Timeout],
[DETypes_1.ObjectStatus.Failed]: [DETypes_1.ObjectStatus.Ready, DETypes_1.ObjectStatus.Loading],
[DETypes_1.ObjectStatus.Timeout]: [DETypes_1.ObjectStatus.Ready, DETypes_1.ObjectStatus.Loading]
};
const rejectStatus = (targetStatus) => {
const rejStatus = rejectStatusMap[targetStatus] ?? [];
return rejStatus;
};
/**
* 数据对象类
* 实现了 IData 接口,提供数据的存储、加载、更新和状态管理功能
*/
class DEData {
tag;
#autoSaveTimerId;
#storeObj;
#data = {};
#tempState = null;
#options;
#temp = {};
#dirty = null;
#hasImage = false;
#loadingPromise;
#loadingTimeoutId;
#retryLaterId;
#destroyed = false;
constructor(tag, options, image) {
if (tag instanceof DETypes_1.Tag) {
this.tag = tag;
}
else {
this.tag = new DETypes_1.Tag(tag);
}
if (!dataStateMap.has(this.tag.path)) {
dataStateMap.set(this.tag.path, this);
}
if (!statusWaitersMap.has(this.tag.path)) {
statusWaitersMap.set(this.tag.path, new Map());
}
this.#options = options;
if (this.#options.state) {
this.#tempState = this.#options.state;
}
if (image) {
this.#hasImage = true;
this.#storeObj = image;
}
this.#init();
}
get dirtyDataPath() {
return `${this.#options.database ?? '*'}.${this.tag.path}`;
}
get statusWaiters() {
if (!statusWaitersMap.has(this.tag.path)) {
statusWaitersMap.set(this.tag.path, new Map());
}
return statusWaitersMap.get(this.tag.path);
}
#init = () => {
if (!this.#options.loadingMode) {
this.#options.loadingMode = 'localDB';
}
if (this.options.saveDirtyData === true) {
this.#loadDirtyDataFromSession();
}
const tempState = dataTempStateMap.get(this.tag.path);
if (tempState) {
this.#storeObj = { ...this.#storeObj, ...tempState };
if (this.#options.default) {
this.#storeObj.default = (0, lodash_1.assign)(tempState.default ?? {}, this.#options.default);
}
try {
if (!this.#storeObj.expires && this.options.expireTime) {
this.#storeObj.expires = new Date().valueOf() + this.options.expireTime;
}
if (this.#storeObj.status === DETypes_1.ObjectStatus.Ready) {
this.#storeObj.status = DETypes_1.ObjectStatus.Loading;
dataTempStateMap.delete(this.tag.path);
this.#setStatus(DETypes_1.ObjectStatus.Ready);
this.#mergeData();
return;
}
if (this.#options.autoLoad !== false) {
this.load();
}
}
catch (err) {
(0, DEHub_1.error)(this, err);
}
}
else {
const dataObj = this.#storeObj ?? {
data: {},
state: {},
status: DETypes_1.ObjectStatus.New,
default: this.#options.default,
virtual: {},
expires: new Date().valueOf() + (this.#options.expireTime ?? 5000),
retryCount: 0
};
this.#storeObj = dataObj;
if (this.#options.autoLoad !== false) {
this.load();
}
}
this.#mergeData();
};
#save = (immediate = false) => {
this.#mergeData();
return new Promise(async (resolve, reject) => {
if (Object.values(this.#storeObj.data).length === 0
&& Object.values(this.#storeObj.virtual).length === 0) {
return resolve();
}
const saveAction = async () => {
if (this.#options.loadingMode !== 'none') {
try {
await (0, idb_keyval_1.set)(this.tag.path, this.#storeObj, this.database);
this.#tempState = null;
}
catch (err) {
return reject(err);
}
}
};
if (this.#autoSaveTimerId)
clearTimeout(this.#autoSaveTimerId);
if (immediate) {
await saveAction();
}
else {
this.#autoSaveTimerId = setTimeout(saveAction, 3000);
}
return resolve();
});
};
#retryLater = () => {
if (!this.#options.retryInterval) {
return;
}
const retryLimit = this.#options.retryLimit ?? 5;
if ((this.#storeObj.retryCount ?? 0) > retryLimit) {
return;
}
this.#storeObj.retryCount = (this.#storeObj.retryCount ?? 0) + 1;
if (this.#retryLaterId)
clearTimeout(this.#retryLaterId);
this.#retryLaterId = setTimeout(() => {
switch (this.#storeObj.retryStatus) {
case DETypes_1.ObjectStatus.New:
case DETypes_1.ObjectStatus.Loading:
this.#storeObj.status = DETypes_1.ObjectStatus.New;
this.load(true);
break;
case DETypes_1.ObjectStatus.Ready:
if (this.#retryLaterId)
clearTimeout(this.#retryLaterId);
break;
}
}, this.#options.retryInterval * this.#storeObj.retryCount);
};
get expires() {
return this.#storeObj.expires;
}
get keys() {
return Object.keys(this.data);
}
get default() {
if (!this.#storeObj.default) {
this.#storeObj.default = {};
}
return this.#storeObj.default;
}
get state() {
if (this.status !== DETypes_1.ObjectStatus.Ready || this.#tempState != null) {
return (0, lodash_1.assign)(this.#storeObj.state ?? {}, this.#tempState);
}
return this.#storeObj.state;
}
updateState = async (state) => {
if (this.status !== DETypes_1.ObjectStatus.Ready && state != null) {
this.#tempState = state;
}
this.#storeObj.state = (0, lodash_1.assign)(this.#storeObj.state ?? {}, state);
const emitContext = {
sender: this,
detail: { value: state },
event: DETypes_1.EventNames.StateChanging,
stage: DETypes_1.EventStage.PostOperation,
property: 'state'
};
const [emitResult, emitPromises] = (0, DEHub_1.emit)(emitContext);
if (emitResult.cancel !== true) {
if (emitPromises && emitPromises.length > 0) {
const asyncResults = await Promise.allSettled(emitPromises);
(0, DETypes_1.rejectedCheck)(asyncResults);
}
}
this.#tempState = null;
};
get storeObject() {
return this.#storeObj;
}
set storeObject(state) {
this.#storeObj = state;
if (this.storeObject.expires && this.storeObject.expires < new Date().valueOf()) {
this.#retryLater();
}
}
until = (status) => {
return new Promise((resolve, reject) => {
const canRes = resolveStatus(status);
if (canRes.indexOf(this.status) >= 0) {
return resolve();
}
const canRej = rejectStatus(status);
if (canRej && canRej.indexOf(this.status) >= 0) {
return reject(new Error(`Status already at incompatible state: ${this.status}`));
}
const promiseGroup = this.statusWaiters.get(status) ?? [];
promiseGroup.push([resolve, reject]);
this.statusWaiters.set(status, promiseGroup);
});
};
reset = async () => {
await this.until(DETypes_1.ObjectStatus.Ready);
await this.#setStatus(DETypes_1.ObjectStatus.Reset);
if (this.options.saveDirtyData === true) {
this.dirty = null;
}
this.#dirty = {};
sessionStorage.removeItem(this.dirtyDataPath);
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
this.#mergeData();
};
clear = async () => {
await this.until(DETypes_1.ObjectStatus.Ready);
await this.#setStatus(DETypes_1.ObjectStatus.Clear);
sessionStorage.removeItem(this.dirtyDataPath);
this.#storeObj.data = {};
await (0, idb_keyval_1.del)(this.tag.path, this.database);
await this.#setStatus(DETypes_1.ObjectStatus.New);
this.#mergeData();
};
get database() {
const db = (0, exports.getStore)(this.#options.database);
return db;
}
get = (attribute) => {
return this.data[attribute];
};
get options() { return this.#options; }
updateOption(options) {
this.#options = options;
if (options && options.default) {
this.#storeObj.default = options.default;
}
if (options && options.state) {
this.#tempState = options.state;
}
this.#mergeData();
}
#customLoadData = () => {
return new Promise(async (resolve, reject) => {
const emitContext = {
sender: this,
status: DETypes_1.ObjectStatus.Loading,
event: DETypes_1.EventNames.Submit,
stage: DETypes_1.EventStage.PreOperation,
database: this.#options.database
};
try {
const [loadingContext, loadingTasks] = (0, DEHub_1.emit)(emitContext);
if (loadingContext.cancel !== true) {
if (loadingTasks && loadingTasks.length > 0) {
const asyncResults = await Promise.allSettled(loadingTasks);
(0, DETypes_1.rejectedCheck)(asyncResults);
}
}
else {
this.#setStatus(DETypes_1.ObjectStatus.Ready);
return reject(loadingContext);
}
if (loadingContext.output?.data) {
this.original = loadingContext.output.data;
}
if (loadingContext.output?.default) {
this.#storeObj.default = loadingContext.output.default;
}
if (loadingContext.output?.state) {
await this.updateState(loadingContext.output.state);
}
this.#mergeData();
const postContext = { ...emitContext, stage: DETypes_1.EventStage.PostOperation };
(0, DEHub_1.emit)(postContext);
return resolve();
}
catch (err) {
(0, DEHub_1.error)(this, err);
return reject(err);
}
});
};
load = (force = false) => {
if (this.#loadingPromise && !force) {
return this.#loadingPromise;
}
this.#loadingPromise = this.#loadDataProcess(force)
.finally(() => {
this.#cleanupLoaderResource();
});
return this.#loadingPromise;
};
#cleanupLoaderResource() {
if (this.#loadingTimeoutId) {
clearTimeout(this.#loadingTimeoutId);
this.#loadingTimeoutId = undefined;
}
this.#loadingPromise = undefined;
}
#loadDataProcess = (force) => new Promise(async (resolve, reject) => {
const loadingMode = this.#options.loadingMode;
switch (loadingMode) {
case 'localDB':
case 'custom':
try {
const tempData = dataTempStateMap.get(this.tag.path);
let localData = null;
if (tempData && this.#hasImage) {
localData = this.#storeObj;
}
else if (tempData) {
localData = tempData;
}
else if (this.#hasImage) {
localData = this.#storeObj;
}
else {
localData = await (0, exports.getFromDB)(this.tag, this.#options.database);
}
if (!force && !tempData && this.#hasImage !== true && this.status === DETypes_1.ObjectStatus.Loading) {
await this.until(DETypes_1.ObjectStatus.Ready);
return resolve(this.data);
}
if (localData) {
!tempData && (this.storeObject = localData);
if (this.#options.default &&
!(0, lodash_1.isEqual)(this.#storeObj.default, this.#options.default)) {
this.#storeObj.default = (0, lodash_1.merge)(this.default, this.#options.default);
}
if (this.storeObject.expires && this.storeObject.expires < new Date().valueOf()) {
force = true;
}
else {
switch (this.#storeObj.status) {
case DETypes_1.ObjectStatus.New:
force = true;
break;
case DETypes_1.ObjectStatus.Ready:
if (!force) {
if (this.expires && this.expires < new Date().valueOf()) {
this.#customLoadData();
}
this.#storeObj.status = DETypes_1.ObjectStatus.Loading;
this.#mergeData();
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
return resolve(this.data);
}
break;
case DETypes_1.ObjectStatus.Loading:
if (!force || this.#loadingTimeoutId) {
await this.until(DETypes_1.ObjectStatus.Ready);
return resolve(this.data);
}
break;
case DETypes_1.ObjectStatus.Deleted:
this.#storeObj.status = DETypes_1.ObjectStatus.Deleting;
await this.#setStatus(DETypes_1.ObjectStatus.Deleted);
return reject(new Error(`数据${this.tag.path}已删除`));
default:
this.#storeObj.status = DETypes_1.ObjectStatus.New;
force = true;
break;
}
}
}
await this.#setStatus(DETypes_1.ObjectStatus.Loading);
if (this.#loadingTimeoutId) {
await this.until(DETypes_1.ObjectStatus.Ready);
return resolve(this.data);
}
if (!localData || force === true) {
this.#loadingTimeoutId = setTimeout(() => {
if (this.#storeObj.status === DETypes_1.ObjectStatus.Loading) {
this.#setStatus(DETypes_1.ObjectStatus.Timeout);
}
this.#cleanupLoaderResource();
}, this.#options.timeout || LOADING_TIMEOUT);
await this.#customLoadData();
}
this.#cleanupLoaderResource();
setTimeout(() => {
dataTempStateMap.delete(this.tag.path);
}, TEMP_DATA_CLEANUP_DELAY);
this.#mergeData();
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
return resolve(this.data);
}
catch (reason) {
this.#cleanupLoaderResource();
console.error('[DEData] load failed:', reason);
this.#setStatus(DETypes_1.ObjectStatus.Failed);
return reject(reason);
}
default:
if (this.status === DETypes_1.ObjectStatus.Ready) {
setTimeout(() => {
dataTempStateMap.delete(this.tag.path);
}, TEMP_DATA_CLEANUP_DELAY);
return resolve(this.data);
}
this.#setStatus(DETypes_1.ObjectStatus.Loading);
try {
await this.#customLoadData();
this.#mergeData();
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
return resolve(this.data);
}
catch (err) {
await this.#setStatus(DETypes_1.ObjectStatus.Failed);
return reject(err);
}
}
});
#setStatus = (statusValue) => {
const preStatus = this.status;
if (statusValue === DETypes_1.ObjectStatus.Ready && this.#tempState) {
this.#storeObj.state = (0, lodash_1.assign)(this.#storeObj.state, this.#tempState);
}
return new Promise(async (resolve, reject) => {
if (preStatus === statusValue) {
return resolve();
}
this.storeObject.status = statusValue;
if ([DETypes_1.ObjectStatus.Ready,
DETypes_1.ObjectStatus.Deleting,
DETypes_1.ObjectStatus.Loading,
DETypes_1.ObjectStatus.Uploading,
DETypes_1.ObjectStatus.Timeout,
DETypes_1.ObjectStatus.Failed].indexOf(statusValue) >= 0) {
try {
await this.#save(true);
}
catch (err) {
(0, DEHub_1.error)(this, err);
return reject(err);
}
}
if ([DETypes_1.ObjectStatus.Failed, DETypes_1.ObjectStatus.Timeout].indexOf(statusValue) >= 0) {
this.storeObject.retryStatus = preStatus;
this.#retryLater();
}
if ([DETypes_1.ObjectStatus.Deleted,
DETypes_1.ObjectStatus.Ready,
DETypes_1.ObjectStatus.Failed,
DETypes_1.ObjectStatus.Timeout].indexOf(statusValue) >= 0) {
const resStatus = resolveStatus(statusValue);
for (const tStatus of resStatus) {
const promiseGroup = this.statusWaiters.get(tStatus) ?? [];
if (promiseGroup.length === 0) {
continue;
}
for (const [resStateResolve] of promiseGroup) {
try {
resStateResolve();
}
catch (err) {
(0, DEHub_1.error)(this, err);
}
}
this.statusWaiters.delete(tStatus);
}
const rejStatus = this.statusWaiters.keys();
for (const tStatus of rejStatus) {
const promiseGroup = this.statusWaiters.get(tStatus) ?? [];
if (promiseGroup.length > 0) {
for (const [, resStateReject] of promiseGroup) {
try {
resStateReject(new Error(`${this.tag.path}的状态已变更为${statusValue}`));
}
catch (e) { /* ignore */ }
}
}
this.statusWaiters.delete(tStatus);
}
}
if (this.#storeObj.retryCount && this.#storeObj.retryCount > 0
&& [DETypes_1.ObjectStatus.Deleted, DETypes_1.ObjectStatus.Ready].indexOf(statusValue) >= 0) {
this.#storeObj.retryCount = 0;
}
const updateArgs = {
value: statusValue,
preImage: preStatus,
postImage: statusValue
};
const emitContext = {
sender: this,
status: statusValue,
event: DETypes_1.EventNames.StatusChanging,
stage: DETypes_1.EventStage.PostOperation,
detail: updateArgs,
database: this.#options.database
};
try {
const [emitResult, emitPromises] = (0, DEHub_1.emit)(emitContext);
if (emitPromises && emitPromises.length > 0) {
(0, DETypes_1.rejectedCheck)(await Promise.allSettled(emitPromises));
}
return resolve();
}
catch (err) {
console.error('[DEData] setStatus emit error:', err);
return reject(err);
}
});
};
get status() {
if (this.#loadingTimeoutId != null
&& !this.#loadingPromise
&& this.storeObject.status === DETypes_1.ObjectStatus.Loading) {
return DETypes_1.ObjectStatus.New;
}
return this.storeObject.status;
}
get original() {
const original = (0, lodash_1.assign)({ ...this.default }, { ...this.storeObject.data });
return original;
}
set original(original) {
this.storeObject.data = original;
this.storeObject.expires = new Date().valueOf() + (this.#options.expireTime ?? 10000);
}
#mergeData = () => {
if (this.#tempState) {
const newState = (0, lodash_1.assign)(this.#storeObj.state ?? {}, this.#tempState);
this.#storeObj.state = newState;
}
this.#data = (0, lodash_1.assign)({ ...this.virtual }, (0, lodash_1.assign)({ ...this.original }, (0, lodash_1.assign)({ ...this.dirty }, this.#temp)));
};
get data() {
return this.#data;
}
#loadDirtyDataFromSession = () => {
const dirtyStr = sessionStorage.getItem(this.dirtyDataPath);
if (!dirtyStr) {
return {};
}
try {
const dirtyObj = JSON.parse(dirtyStr);
this.#dirty = dirtyObj;
return dirtyObj;
}
catch (err) {
(0, DEHub_1.error)(this, err);
return {};
}
};
get dirty() {
return this.#dirty ?? {};
}
set dirty(dirty) {
if (this.#options.saveDirtyData === true) {
try {
if (dirty == null) {
sessionStorage.removeItem(this.dirtyDataPath);
}
else {
const newDirty = (0, DETypes_1.clearNullNodes)((0, lodash_1.assign)(this.#dirty, dirty));
this.#dirty = newDirty;
sessionStorage.setItem(this.dirtyDataPath, JSON.stringify(newDirty));
this.#mergeData();
}
}
catch (err) {
(0, DEHub_1.error)(this, `数据${JSON.stringify(this.tag)}转换值失败`);
throw err;
}
}
else {
this.#dirty = (0, lodash_1.assign)(this.#dirty, dirty);
}
}
get virtual() {
return this.#storeObj.virtual;
}
#setActionVer = 0;
#attributeVer = new Map();
set = (attribute, value, isVirtual) => {
const setVer = this.#setActionVer++;
return new Promise(async (resolve, reject) => {
if ((0, lodash_1.isEqual)(this.data[attribute], value)) {
return resolve(value);
}
const preValue = this.data[attribute];
this.#temp[attribute] = value;
this.#attributeVer.set(attribute, setVer);
this.#mergeData();
const target = { [attribute]: value };
const dataContext = {
value: target,
preImage: { [attribute]: preValue }
};
const emitContext = {
sender: this,
attribute,
event: DETypes_1.EventNames.ValueChanged,
stage: DETypes_1.EventStage.PreOperation,
detail: dataContext,
database: this.#options.database
};
try {
const [setvalueContext, setValuePromises] = (0, DEHub_1.emit)(emitContext);
if (setvalueContext.cancel === true) {
return reject(setvalueContext);
}
if (setvalueContext.output.value) {
this.#temp[attribute] = setvalueContext.output.value;
dataContext.value = setvalueContext.output.value;
value = setvalueContext.output.value;
target[attribute] = setvalueContext.output.value;
}
(0, DETypes_1.rejectedCheck)(await Promise.allSettled(setValuePromises));
const mergedValue = (0, lodash_1.assign)((isVirtual === true ? this.virtual : this.dirty) ?? {}, target);
if (this.#attributeVer.get(attribute) !== setVer) {
mergedValue[attribute] = this.data[attribute];
}
if (isVirtual === true) {
if (this.#storeObj.data[attribute] !== undefined) {
delete this.#storeObj.data[attribute];
}
this.#storeObj.virtual = mergedValue;
}
else {
this.dirty = mergedValue;
}
this.#mergeData();
}
catch (err) {
return reject(err);
}
finally {
delete this.#temp[attribute];
}
dataContext.postImage = { [attribute]: this.data[attribute] };
const postContext = { ...emitContext, stage: DETypes_1.EventStage.PostOperation };
try {
const [postResult, postFuncs] = (0, DEHub_1.emit)(postContext);
if (postFuncs && postFuncs.length > 0) {
(0, DETypes_1.rejectedCheck)(await Promise.allSettled(postFuncs));
}
resolve(this.data[attribute]);
}
catch (err) {
(0, DEHub_1.error)(this, err);
reject(err);
}
});
};
update = (data, noTrigger = false) => {
let updatedObj = {};
const target = data.data ?? data;
for (const att of Object.keys(target)) {
const attVal = target[att];
if ((0, lodash_1.isEqual)(attVal, this.get(att))) {
continue;
}
if (noTrigger === true) {
this.#storeObj.data[att] = attVal;
}
else {
this.set(att, attVal);
}
updatedObj[att] = attVal;
}
this.#save(true);
return updatedObj;
};
submit = (target = {}, mergeData = true) => {
return new Promise(async (resolve, reject) => {
if (Object.values(this.dirty).length === 0 && Object.keys(target).length === 0) {
await this.#save();
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
return resolve(target);
}
try {
await this.#setStatus(DETypes_1.ObjectStatus.Uploading);
}
catch (err) {
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
return reject(err);
}
const submitValue = { ...this.dirty, ...target };
const preImage = { ...this.original };
const updateArgs = {
value: submitValue,
preImage: preImage
};
const emitContext = {
sender: this,
event: DETypes_1.EventNames.Submit,
status: DETypes_1.ObjectStatus.Uploading,
stage: DETypes_1.EventStage.PreOperation,
detail: updateArgs,
database: this.#options.database
};
let submitTimerId;
try {
if (this.#options.loadingMode === 'custom' && this.#options.timeout) {
submitTimerId = setTimeout(() => {
if (this.status === DETypes_1.ObjectStatus.Uploading) {
this.#setStatus(DETypes_1.ObjectStatus.Timeout);
}
reject(new Error('submit timeout'));
}, this.#options.timeout);
}
const [eventContext, emitPromises] = (0, DEHub_1.emit)(emitContext);
const asyncResults = await Promise.allSettled(emitPromises);
(0, DETypes_1.rejectedCheck)(asyncResults);
const updatedData = eventContext.output.data ?? updateArgs.value;
this.original = mergeData ? (0, DETypes_1.mergeObject)(this.original, updatedData) : updatedData;
this.#dirty = {};
if (this.options.saveDirtyData === true) {
this.dirty = null;
}
sessionStorage.removeItem(this.dirtyDataPath);
await this.#save(true);
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
}
catch (err) {
await this.#setStatus(DETypes_1.ObjectStatus.Failed);
return reject(err);
}
finally {
if (submitTimerId)
clearTimeout(submitTimerId);
}
updateArgs.postImage = { ...this.original };
const postContext = { ...emitContext, stage: DETypes_1.EventStage.PostOperation };
try {
const [postResult, postTasks] = (0, DEHub_1.emit)(postContext);
if (postTasks && postTasks.length > 0) {
(0, DETypes_1.rejectedCheck)(await Promise.allSettled(postTasks));
}
return resolve(updateArgs.postImage);
}
catch (err) {
return reject(err);
}
});
};
delete = () => {
return new Promise(async (resolve, reject) => {
try {
await this.until(DETypes_1.ObjectStatus.Ready);
await this.#setStatus(DETypes_1.ObjectStatus.Deleting);
}
catch (err) {
return reject(err);
}
const updateArgs = {
value: { ...this.dirty },
preImage: { ...this.original }
};
const emitContext = {
sender: this,
event: DETypes_1.EventNames.Submit,
status: DETypes_1.ObjectStatus.Deleting,
stage: DETypes_1.EventStage.PreOperation,
detail: updateArgs,
database: this.#options.database
};
try {
const [eventContext, emitPromises] = (0, DEHub_1.emit)(emitContext);
if (emitPromises && emitPromises.length > 0) {
await Promise.allSettled(emitPromises);
}
if (eventContext.cancel !== true) {
if (this.#options.loadingMode !== 'none') {
await (0, idb_keyval_1.del)(this.tag.path, this.database);
sessionStorage.removeItem(this.dirtyDataPath);
dataStateMap.delete(this.tag.path);
}
await this.#setStatus(DETypes_1.ObjectStatus.Deleted);
}
else {
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
reject(eventContext);
}
this.#mergeData();
}
catch (err) {
await this.#setStatus(DETypes_1.ObjectStatus.Ready);
return reject(err);
}
const postContext = { ...emitContext, stage: DETypes_1.EventStage.PostOperation };
try {
const [postResult, postPromises] = (0, DEHub_1.emit)(postContext);
if (postPromises && postPromises.length > 0) {
await Promise.allSettled(postPromises);
}
return resolve();
}
catch (err) {
return reject(err);
}
});
};
destroy = async () => {
if (this.#destroyed)
return;
this.#destroyed = true;
if (this.#autoSaveTimerId)
clearTimeout(this.#autoSaveTimerId);
if (this.#retryLaterId)
clearTimeout(this.#retryLaterId);
this.#cleanupLoaderResource();
const waiters = statusWaitersMap.get(this.tag.path);
if (waiters) {
for (const group of waiters.values()) {
for (const [, rejectFn] of group) {
try {
rejectFn(new Error('Data object destroyed'));
}
catch (e) { /* ignore */ }
}
}
statusWaitersMap.delete(this.tag.path);
}
dataStateMap.delete(this.tag.path);
dataTempStateMap.delete(this.tag.path);
try {
sessionStorage.removeItem(this.dirtyDataPath);
}
catch (e) { /* ignore */ }
};
}
const getData = (tag) => {
let targetData;
if (tag instanceof DETypes_1.Tag) {
targetData = dataStateMap.get(tag.path);
}
else if (typeof tag === 'string') {
targetData = dataStateMap.get(tag);
}
else {
targetData = dataStateMap.get(DETypes_1.TagsSerializer.toPath(tag));
}
return targetData;
};
exports.getData = getData;
const fetchData = async (tag, options, waitToReady = false) => {
let tTag;
if (tag instanceof DETypes_1.Tag) {
tTag = tag;
}
else if (typeof tag === 'string') {
tTag = new DETypes_1.Tag(DETypes_1.TagsSerializer.fromPath(tag));
}
else {
tTag = new DETypes_1.Tag(tag);
}
let dataTarget = dataStateMap.get(tTag.path);
if (dataTarget != null) {
(dataTarget.status === DETypes_1.ObjectStatus.New
|| dataTarget.status === DETypes_1.ObjectStatus.Failed
|| dataTarget.status === DETypes_1.ObjectStatus.Reset
|| dataTarget.status === DETypes_1.ObjectStatus.Timeout) && dataTarget.load();
if (waitToReady)
await dataTarget.until(DETypes_1.ObjectStatus.Ready);
if (options) {
dataTarget.updateOption((0, DETypes_1.mergeObject)((dataTarget.options ?? {}), options));
}
return dataTarget;
}
dataTarget = dataStateMap.get(tTag.path);
let targetOptions = { ...((dataTarget ? dataTarget.options : options) ?? {}) };
const emitSender = {
tag: tTag,
options: targetOptions,
image: dataTarget ? dataTarget.data : null
};
const emitContext = {
sender: emitSender,
detail: emitSender,
status: DETypes_1.ObjectStatus.New,
event: DETypes_1.EventNames.StatusChanging,
stage: DETypes_1.EventStage.PreOperation,
database: targetOptions.database
};
let prePromiseResults = [];
let emitResult = null;
let preEmitPromises = [];
try {
[emitResult, preEmitPromises] = (0, DEHub_1.emit)(emitContext);
if (preEmitPromises.length > 0) {
prePromiseResults = await Promise.allSettled(preEmitPromises);
(0, DETypes_1.rejectedCheck)(prePromiseResults);
}
}
catch (err) {
(0, DEHub_1.error)(emitSender, err);
throw err;
}
if (emitResult.output.options) {
targetOptions = (0, lodash_1.assign)(targetOptions, emitResult.output.options);
}
let defaultValue = targetOptions.default ?? {};
if (emitResult.output.default) {
defaultValue = (0, lodash_1.assign)(defaultValue ?? {}, emitResult.output.default ?? {});
targetOptions.default = defaultValue;
}
if (emitResult.output.state) {
targetOptions.state = emitResult.output.state;
}
let localState;
if (targetOptions?.loadingMode === 'none') {
localState = undefined;
}
else {
localState = await (0, exports.getFromDB)(tTag, targetOptions?.database);
}
try {
dataTarget = (0, exports.getData)(tTag);
if (!dataTarget) {
if (localState?.status === DETypes_1.ObjectStatus.Loading) {
localState.status = DETypes_1.ObjectStatus.New;
}
dataTarget = new DEData(tTag, targetOptions ?? { loadingMode: 'localDB' }, localState);
}
const postEmitContent = { ...emitContext, sender: dataTarget, stage: DETypes_1.EventStage.PostOperation };
const [postEmitResult, postEmitPromises] = (0, DEHub_1.emit)(postEmitContent);
if (postEmitPromises.length > 0) {
const postPromiseResults = await Promise.allSettled(postEmitPromises);
(0, DETypes_1.rejectedCheck)(postPromiseResults);
}
}
catch (err) {
(0, DEHub_1.error)(emitSender, err);
throw err;
}
dataTarget = (0, exports.getData)(tTag);
if (waitToReady === true) {
await dataTarget.until(DETypes_1.ObjectStatus.Ready);
}
return dataTarget;
};
exports.default = fetchData;