UNPKG

dehub

Version:
1,148 lines (1,147 loc) 46.5 kB
"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;