import fs from 'fs'
import get from 'lodash-es/get.js'
import debounce from 'lodash-es/debounce.js'
import merge from 'lodash-es/merge.js'
import now2str from 'wsemi/src/now2str.mjs'
import genID from 'wsemi/src/genID.mjs'
import evem from 'wsemi/src/evem.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import j2o from 'wsemi/src/j2o.mjs'
import o2j from 'wsemi/src/o2j.mjs'
/**
* 伺服器端之資料同步器
*
* @class
* @param {Object} instWConverServer 輸入通訊服務實體物件,可使用例如WConverhpServer等建立
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.fpTableTags='tableTags.json'] 輸入儲存各資料表時間戳檔案路徑串,預設'./tableTags.json'
* @param {Function} [opt.genTag=()=>'{random string}'] 輸入產生不重複識別碼函數,預設()=>'{random string}'
* @returns {Object} 回傳後端資料同步物件,可監聽事件changeTableTags、error,可使用函數readTableTags、writeTableTags、initTableTags、setTableTags、getTableTags、updateTableTag
* @example
*
* //前後端共用範例
* import w from 'wsemi'
* import WSyncWebdataServer from './src/WSyncWebdataServer.mjs'
* import WSyncWebdataClient from './src/WSyncWebdataClient.mjs'
*
* let ms = []
*
* let ntg = 0
* let genTag = () => {
* ntg++
* return `tag-${ntg}`
* }
*
* //-------- 後端 ---------
*
* //EventEmitter, 模擬後端推播至前端
* let ee = w.evem()
*
* //instWConverServer
* let instWConverServer = w.evem()
*
* //wsds
* let wsds = new WSyncWebdataServer(
* instWConverServer,
* {
* // fpTableTags: 'tableTags-sync-webdata.json',
* genTag,
* },
* )
*
* //tableTagsSrv
* let tableTagsSrv = {
* tabA: 'tag-0-a',
* tabB: 'tag-0-b',
* }
*
* //initTableTags
* wsds.initTableTags(tableTagsSrv)
*
* //readTableTags
* console.log('server: nowTableTags', wsds.readTableTags())
* ms.push({ 'server nowTableTags': JSON.stringify(wsds.readTableTags()) })
*
* //updateTableTag
* setTimeout(() => {
* wsds.updateTableTag('tabA')
* ms.push({ 'server updateTableTag': 'tabA' })
* }, 500)
* setTimeout(() => {
* wsds.updateTableTag('tabB')
* ms.push({ 'server updateTableTag': 'tabB' })
* }, 750)
* setTimeout(() => {
* wsds.updateTableTag('tabA')
* ms.push({ 'server updateTableTag': 'tabA' })
* }, 1000)
*
* //changeTableTags
* wsds.on('changeTableTags', (nowTableTags) => {
* console.log('server: changeTableTags', nowTableTags)
*
* //server push
* console.log('server: push')
* ms.push({ 'server changeTableTags': JSON.stringify(nowTableTags) })
* ee.emit('push', nowTableTags)
*
* })
*
* //error
* wsds.on('error', (err) => {
* console.log('server: error', err)
* })
*
* //getAPIData, 模擬後端提供API
* let tabsCount = {
* tabA: 0,
* tabB: 0,
* }
* async function getAPIData(tableName) {
* ms.push({ 'server call getAPIData before': tableName })
* return new Promise((resolve, reject) => {
* setTimeout(() => {
* tabsCount[tableName] += 1
* ms.push({ 'server call getAPIData after': `table[${tableName}] = ${tabsCount[tableName]}` })
* resolve(`table[${tableName}] = ${tabsCount[tableName]}`)
* }, 100)
* })
* }
*
* //-------- 前端 ---------
*
* //instWConverClient
* let instWConverClient = w.evem()
*
* //wsdc
* let wsdc = new WSyncWebdataClient(instWConverClient, {})
*
* //tableTagsCl
* let tableTagsCl = {
* tabA: 'tag-0-a',
* tabB: 'tag-0-b',
* }
*
* //setTableTags, 模擬前端將tableTags預先或有變更即儲存至localStorage, 啟動時有既有tableTags
* wsdc.setTableTags(tableTagsCl)
* ms.push({ 'clinet setTableTags': JSON.stringify(tableTagsCl) })
*
* //push, 模擬前端接收後端推播有變更tableTags
* ee.on('push', (nowTableTags) => {
*
* //updateTableTags
* wsdc.updateTableTags(nowTableTags)
* ms.push({ 'clinet updateTableTags': JSON.stringify(nowTableTags) })
*
* })
*
* //refreshState
* wsdc.on('refreshState', (msg) => {
* console.log('client: refreshState needToRefresh', msg.needToRefresh)
* })
*
* //refreshTable
* wsdc.on('refreshTable', (input) => {
* // console.log('client: refreshTable', input)
*
* //收到有變更tableTags, 模擬呼叫後端API
* console.log('client: getAPIData before: ' + input.tableName)
* ms.push({ 'clinet call getAPIData before': input.tableName })
* getAPIData(input.tableName)
* .then((data) => {
* console.log('client: getAPIData after: ' + data)
* ms.push({ 'clinet call getAPIData after': data })
* input.pm.resolve(data) //Use pm.resolve to retrieve the data, and pm.reject the message when get an error.
* })
* .catch((err) => {
* input.pm.reject(err)
* })
*
* })
*
* //getData
* wsdc.on('getData', (data) => {
* console.log('client: getData', data)
* //前端取得數據
* })
*
* //beforeUpdateTableTags, afterUpdateTableTags
* wsdc.on('beforeUpdateTableTags', (msg) => {
* // console.log('client: beforeUpdateTableTags', msg)
* })
* wsdc.on('afterUpdateTableTags', (msg) => {
* // console.log('client: afterUpdateTableTags', msg)
* })
*
* //error
* wsdc.on('error', (err) => {
* console.log('client: error', err)
* })
*
* setTimeout(() => {
* console.log('ms', ms)
* }, 2000)
*
* // server: nowTableTags { tabA: 'tag-0-a', tabB: 'tag-0-b' }
* // server: changeTableTags { tabA: 'tag-1', tabB: 'tag-0-b' }
* // server: push
* // client: refreshState needToRefresh true
* // client: getAPIData before: tabA
* // client: getAPIData after: table[tabA] = 1
* // client: getData { tableName: 'tabA', timeTag: 'tag-1', data: 'table[tabA] = 1' }
* // server: changeTableTags { tabA: 'tag-1', tabB: 'tag-2' }
* // server: push
* // client: refreshState needToRefresh true
* // client: getAPIData before: tabB
* // client: getAPIData after: table[tabB] = 1
* // client: getData { tableName: 'tabB', timeTag: 'tag-2', data: 'table[tabB] = 1' }
* // server: changeTableTags { tabA: 'tag-3', tabB: 'tag-2' }
* // server: push
* // client: refreshState needToRefresh true
* // client: getAPIData before: tabA
* // client: getAPIData after: table[tabA] = 2
* // client: getData { tableName: 'tabA', timeTag: 'tag-3', data: 'table[tabA] = 2' }
* // ms [
* // { 'server nowTableTags': '{"tabA":"tag-0-a","tabB":"tag-0-b"}' },
* // { 'clinet setTableTags': '{"tabA":"tag-0-a","tabB":"tag-0-b"}' },
* // { 'server updateTableTag': 'tabA' },
* // { 'server changeTableTags': '{"tabA":"tag-1","tabB":"tag-0-b"}' },
* // { 'clinet updateTableTags': '{"tabA":"tag-1","tabB":"tag-0-b"}' },
* // { 'clinet call getAPIData before': 'tabA' },
* // { 'server call getAPIData before': 'tabA' },
* // { 'server updateTableTag': 'tabB' },
* // { 'server call getAPIData after': 'table[tabA] = 1' },
* // { 'clinet call getAPIData after': 'table[tabA] = 1' },
* // { 'server changeTableTags': '{"tabA":"tag-1","tabB":"tag-2"}' },
* // { 'clinet updateTableTags': '{"tabA":"tag-1","tabB":"tag-2"}' },
* // { 'clinet call getAPIData before': 'tabB' },
* // { 'server call getAPIData before': 'tabB' },
* // { 'server updateTableTag': 'tabA' },
* // { 'server call getAPIData after': 'table[tabB] = 1' },
* // { 'clinet call getAPIData after': 'table[tabB] = 1' },
* // { 'server changeTableTags': '{"tabA":"tag-3","tabB":"tag-2"}' },
* // { 'clinet updateTableTags': '{"tabA":"tag-3","tabB":"tag-2"}' },
* // { 'clinet call getAPIData before': 'tabA' },
* // { 'server call getAPIData before': 'tabA' },
* // { 'server call getAPIData after': 'table[tabA] = 2' },
* // { 'clinet call getAPIData after': 'table[tabA] = 2' }
* // ]
*
*/
function WSyncWebdataServer(instWConverServer, opt = {}) {
let nowTableTags = {}
//check
if (!iseobj(instWConverServer)) {
console.log('instWConverServer is not an effective object, and set instWConverServer to an EventEmitter')
instWConverServer = evem()
}
if (!haskey(instWConverServer, 'emit')) {
throw new Error(`instWConverServer is not an EventEmitter`)
}
//fpTableTags
let fpTableTags = get(opt, 'fpTableTags', '')
if (!isestr(fpTableTags)) {
fpTableTags = './tableTags.json'
}
//genTag
let genTag = get(opt, 'genTag')
if (!isfun(genTag)) {
genTag = () => {
return now2str() + '|' + genID(6)
}
}
//eeEmit
let eeEmit = (name, ...args) => {
setTimeout(() => {
instWConverServer.emit(name, ...args)
}, 1)
}
/**
* 讀取各資料表時間資料
*
* @memberof WSyncWebdataServer
* @returns {Object} 回傳各資料表時間戳物件
* @example
*
* let tableTags = wsds.readTableTags()
*
*/
function readTableTags() {
let r = {}
try {
if (fs.existsSync(fpTableTags)) {
let c = fs.readFileSync(fpTableTags, 'utf8')
let o = j2o(c)
if (iseobj(o)) {
r = o
}
}
}
catch (err) {
eeEmit('error', {
msg: 'readTableTags catch',
err,
})
}
return r
}
/**
* 儲存各資料表時間資料
*
* @memberof WSyncWebdataServer
* @param {Object} tableTags 輸入各資料表時間戳物件
* @returns {Undefined} 無回傳
* @example
*
* let tableTags = {...}
* wsds.writeTableTags(tableTags)
*
*/
function writeTableTags() {
try {
let c = o2j(nowTableTags)
fs.writeFileSync(fpTableTags, c, 'utf8')
}
catch (err) {
eeEmit('error', {
msg: 'writeTableTags catch',
err,
})
}
}
/**
* 初始化各資料表時間資料
*
* @memberof WSyncWebdataServer
* @param {Object} tableTags 輸入各資料表時間戳物件
* @param {String} [mode='useInputFirst'] 輸入使用設定方式字串,可有'useInputFirst'代表使用傳入設定優先再與既有JSON檔設定合併,為預設值,'useStorageFirst'代表使用既有JSON檔設定優先再與傳入設定合併,'useInputOnly'代表只使用傳入設定,'useStorageOnly'代表只使用既有JSON檔設定
* @returns {Undefined} 無回傳
* @example
*
* let tableTags = {...}
* let mode = ''
* wsds.initTableTags(tableTags, mode)
*
*/
function initTableTags(tableTags = {}, mode = 'useInputFirst') {
// mode可有:
// useInputFirst
// useStorageFirst
// useInputOnly
// useStorageOnly
//mode
if (mode === 'useInputOnly') {
nowTableTags = tableTags
}
else if (mode === 'useStorageOnly') {
nowTableTags = readTableTags()
}
else if (mode === 'useInputFirst') {
nowTableTags = merge(readTableTags(), tableTags)
}
else if (mode === 'useStorageFirst') {
nowTableTags = merge(tableTags, readTableTags())
}
else {
throw new Error(`invalid mode[${mode}]`)
}
//writeTableTags
writeTableTags()
}
/**
* 直接設定各資料表時間資料
*
* @memberof WSyncWebdataServer
* @param {Object} tableTags 輸入各資料表時間戳物件
* @returns {Undefined} 無回傳
* @example
*
* let tableTags = {...}
* wsds.setTableTags(tableTags)
*
*/
function setTableTags(tableTags = {}) {
//merge
nowTableTags = merge(nowTableTags, tableTags)
//writeTableTags
writeTableTags()
}
/**
* 直接取得各資料表時間資料
*
* @memberof WSyncWebdataServer
* @returns {Object} 回傳各資料表時間戳物件
* @example
*
* let tableTags = wsds.getTableTags()
*
*/
function getTableTags() {
return nowTableTags
}
//updateTableTagCore, 避免大量更新時造成大量推播, 通過debounce合併故是回傳nowTableTags
let updateTableTagCore = debounce(() => {
//writeTableTags
writeTableTags()
//emit
eeEmit('changeTableTags', nowTableTags)
}, 200)
/**
* 更新指定資料表之時間戳,當資料表更新時需調用此函數
*
* @memberof WSyncWebdataServer
* @param {String} tableTag 輸入欲更新指定資料表名稱字串
* @returns {Undefined} 無回傳
* @example
*
* let tableName = '...'
* wsds.updateTableTag(tableName)
*
*/
function updateTableTag(tableName) {
//modify
nowTableTags[tableName] = genTag()
//updateTableTagCore
updateTableTagCore()
}
/**
* 監聽更新資料表事件,當外部監聽收到更新通知時再推播nowTableTags至前端
*
* @memberof WSyncWebdataServer
* @param {Object} nowTableTags 各資料表時間戳物件
* @example
*
* wo.on('changeTableTags', function(nowTableTags) {
* ...
* })
*
*/
function onChangeUpdateTableTag() {} onChangeUpdateTableTag()
//save
instWConverServer.readTableTags = readTableTags
instWConverServer.writeTableTags = writeTableTags
instWConverServer.initTableTags = initTableTags
instWConverServer.setTableTags = setTableTags
instWConverServer.getTableTags = getTableTags
instWConverServer.updateTableTag = updateTableTag
return instWConverServer
}
export default WSyncWebdataServer