Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48668d94ad | ||
|
|
e08c431dd9 | ||
|
|
5ee58ad6f0 | ||
|
|
ac0a4f0586 | ||
|
|
b6f4c153e5 | ||
|
|
4fdaf5f555 | ||
|
|
b4ee9d6c00 | ||
|
|
7c73c74730 | ||
|
|
c20aa089fa | ||
|
|
b0e15c22ea | ||
|
|
d58a2c065a | ||
|
|
53135e7ee8 | ||
|
|
5c48ca9e6c | ||
|
|
c4a280f3d8 | ||
|
|
ba2943c722 | ||
|
|
26f6ffc83a | ||
|
|
bcf075a72c | ||
|
|
02d458d192 | ||
|
|
a349d8af68 | ||
|
|
0dbaf32aac | ||
|
|
e8c41ef3a8 | ||
|
|
e43a44e986 | ||
|
|
f14b8ed277 | ||
|
|
bbfe8a64cb | ||
|
|
bcf3c2dab0 | ||
|
|
d5404fd260 | ||
|
|
54bc662e43 | ||
|
|
42546ca97e | ||
|
|
5c13cf0eb9 | ||
|
|
2a9d44ae9a | ||
|
|
38414ae7b6 | ||
|
|
3ecb3e80ac | ||
|
|
4968828488 | ||
|
|
4db3cd24df | ||
|
|
45c6d3da77 | ||
|
|
4aab1da3c6 | ||
|
|
bf5dfa1c15 | ||
|
|
7549bdd2b4 | ||
|
|
1bb2525ab2 |
@@ -12,6 +12,7 @@
|
||||
"@babel"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
@@ -30,6 +31,7 @@
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/no-v-html": "off",
|
||||
"vue/no-v-model-argument": "off",
|
||||
|
||||
"strict": 0,
|
||||
"indent": [0, 4, {
|
||||
|
||||
@@ -5,11 +5,12 @@ const { VueLoaderPlugin } = require('vue-loader');
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
|
||||
module.exports = {
|
||||
/*resolve: {
|
||||
resolve: {
|
||||
alias: {
|
||||
vue: '@vue/compat'
|
||||
ws: false,
|
||||
//vue: '@vue/compat'
|
||||
}
|
||||
},*/
|
||||
},
|
||||
entry: [`${clientDir}/main.js`],
|
||||
output: {
|
||||
publicPath: '/app/',
|
||||
@@ -62,34 +63,6 @@ module.exports = {
|
||||
filename: 'fonts/[name]-[hash:6][ext]'
|
||||
},
|
||||
},
|
||||
/*{
|
||||
test: /\.gif$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.png$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.jpg$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|woff|woff2)$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "fonts/[name]-[hash:6].[ext]"
|
||||
}
|
||||
},*/
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class Reader {
|
||||
const state = response.state;
|
||||
if (!state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (response.state == 'error') {
|
||||
if (state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,16 +55,16 @@
|
||||
|
||||
<div class="col fit tree">
|
||||
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @input="makeTickAll" />
|
||||
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
|
||||
</div>
|
||||
<q-tree
|
||||
v-model:selected="selected"
|
||||
v-model:ticked="ticked"
|
||||
v-model:expanded="expanded"
|
||||
class="q-my-xs"
|
||||
:nodes="nodes"
|
||||
node-key="key"
|
||||
tick-strategy="leaf"
|
||||
v-model:selected="selected"
|
||||
v-model:ticked="ticked"
|
||||
v-model:expanded="expanded"
|
||||
selected-color="black"
|
||||
:filter="search"
|
||||
no-nodes-label="Закладок пока нет"
|
||||
@@ -97,7 +97,7 @@ const componentOptions = {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
ticked: function() {
|
||||
ticked() {
|
||||
this.checkAllTicked();
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<!--div class="address">
|
||||
<img class="logo" src="./assets/paypal.png">
|
||||
<div class="para">
|
||||
{{ paypalAddress }}
|
||||
@@ -29,7 +29,7 @@
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/bitcoin.png">
|
||||
|
||||
@@ -29,14 +29,14 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
|
||||
const pages = {
|
||||
'CommonHelpPage': CommonHelpPage,
|
||||
'HotkeysHelpPage': HotkeysHelpPage,
|
||||
'MouseHelpPage': MouseHelpPage,
|
||||
'VersionHistoryPage': VersionHistoryPage,
|
||||
'DonateHelpPage': DonateHelpPage,
|
||||
//'DonateHelpPage': DonateHelpPage,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
@@ -44,7 +44,7 @@ const tabs = [
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
['DonateHelpPage', 'Помочь проекту'],
|
||||
//['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
const componentOptions = {
|
||||
@@ -73,7 +73,7 @@ class HelpPage {
|
||||
}
|
||||
|
||||
activateDonateHelpPage() {
|
||||
this.selectedTab = 'DonateHelpPage';
|
||||
//this.selectedTab = 'DonateHelpPage';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
|
||||
@@ -33,14 +33,15 @@ class VersionHistoryPage {
|
||||
|
||||
mounted() {
|
||||
let vh = [];
|
||||
for (const version of versionHistory) {
|
||||
vh.push(version.header);
|
||||
for (const v of versionHistory) {
|
||||
vh.push(`${v.version} (${v.releaseDate})`);
|
||||
}
|
||||
this.versionHeader = vh;
|
||||
|
||||
let vc = [];
|
||||
for (const version of versionHistory) {
|
||||
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
||||
for (const v of versionHistory) {
|
||||
let header = `${v.version} (${v.releaseDate})`;
|
||||
vc.push({key: header, content: 'Версия ' + header + v.content});
|
||||
}
|
||||
this.versionContent = vc;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,13 @@
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
|
||||
Загрузить файл с диска
|
||||
</q-btn>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||
Из буфера обмена
|
||||
</q-btn>
|
||||
|
||||
@@ -46,7 +48,7 @@
|
||||
|
||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
|
||||
|
||||
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||
@@ -64,6 +66,7 @@ import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||
|
||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
@@ -114,9 +117,7 @@ class LoaderPage {
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
@@ -138,7 +139,7 @@ class LoaderPage {
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.pasteTextToggle();
|
||||
this.showPasteText();
|
||||
}
|
||||
|
||||
loadBuffer(opts) {
|
||||
@@ -148,6 +149,10 @@ class LoaderPage {
|
||||
}
|
||||
}
|
||||
|
||||
showPasteText() {
|
||||
this.pasteTextActive = true;
|
||||
}
|
||||
|
||||
pasteTextToggle() {
|
||||
this.pasteTextActive = !this.pasteTextActive;
|
||||
}
|
||||
@@ -168,8 +173,9 @@ class LoaderPage {
|
||||
window.open('http://old.omnireader.ru', '_blank');
|
||||
}
|
||||
|
||||
onInputKeydown(event) {
|
||||
async onInputKeydown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
await utils.sleep(100);
|
||||
this.submitUrl();
|
||||
}
|
||||
}
|
||||
@@ -180,14 +186,8 @@ class LoaderPage {
|
||||
}
|
||||
|
||||
const input = this.$refs.input.getNativeElement();
|
||||
if (event.type == 'keydown' && document.activeElement !== input) {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
switch (action) {
|
||||
case 'help':
|
||||
this.openHelp(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,24 @@
|
||||
{{ rstore.readerActions['loader'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
|
||||
<q-icon name="la la-caret-square-up" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['loadFile'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
|
||||
<q-icon name="la la-comment" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['loadBuffer'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
|
||||
<q-icon name="la la-question" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['help'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -89,6 +107,12 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
|
||||
<q-icon name="la la-mouse" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['clickControl'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
||||
<q-icon name="la la-unlink" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
@@ -245,6 +269,8 @@ class Reader {
|
||||
rstore = {};
|
||||
|
||||
loaderActive = false;
|
||||
loadFileActive = false;
|
||||
loadBufferActive = false;
|
||||
fullScreenActive = false;
|
||||
setPositionActive = false;
|
||||
searchActive = false;
|
||||
@@ -254,6 +280,7 @@ class Reader {
|
||||
contentsActive = false;
|
||||
libsActive = false;
|
||||
recentBooksActive = false;
|
||||
clickControlActive = false;
|
||||
offlineModeActive = false;
|
||||
settingsActive = false;
|
||||
|
||||
@@ -310,7 +337,7 @@ class Reader {
|
||||
await this.$nextTick();
|
||||
this.paramPosIgnore = false;
|
||||
}
|
||||
}, 500, {maxWait: 5000});
|
||||
}, 250, {maxWait: 5000});
|
||||
|
||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
@@ -372,6 +399,7 @@ class Reader {
|
||||
this.copyFullText = settings.copyFullText;
|
||||
this.showClickMapPage = settings.showClickMapPage;
|
||||
this.clickControl = settings.clickControl;
|
||||
this.clickControlActive = this.clickControl;
|
||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
@@ -393,6 +421,23 @@ class Reader {
|
||||
this.loadWallpapers();//no await
|
||||
}
|
||||
|
||||
showHelpOnErrorIfNeeded(errorMessage) {
|
||||
//небольшая эвристика
|
||||
let i = errorMessage.indexOf('http://');
|
||||
if (i < 0)
|
||||
i = errorMessage.indexOf('https://');
|
||||
|
||||
errorMessage = errorMessage.substring(i + 7);
|
||||
const perCount = errorMessage.split('%').length - 1;
|
||||
|
||||
if (perCount > errorMessage.length/3.2) {
|
||||
this.$refs.dialogs.showUrlHelp();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//wallpaper css
|
||||
async loadWallpapers() {
|
||||
const wallpaperDataLength = await wallpaperStorage.getLength();
|
||||
@@ -525,9 +570,7 @@ class Reader {
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
get routeParamUrl() {
|
||||
@@ -585,7 +628,20 @@ class Reader {
|
||||
//сохранение в serverStorage
|
||||
if (value) {
|
||||
await utils.sleep(500);
|
||||
await this.$refs.serverStorage.saveRecent(value);
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
if (!this.offlineModeActive)
|
||||
this.$root.notify.error('Таймаут соединения');
|
||||
}, 10000);
|
||||
|
||||
try {
|
||||
await this.$refs.serverStorage.saveRecent(value);
|
||||
} catch (e) {
|
||||
if (!this.offlineModeActive)
|
||||
this.$root.notify.error(e.message);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -646,6 +702,28 @@ class Reader {
|
||||
}
|
||||
}
|
||||
|
||||
loadFileToggle() {
|
||||
if (!this.loaderActive)
|
||||
this.loaderToggle();
|
||||
this.$nextTick(() => {
|
||||
const page = this.$refs.page;
|
||||
if (this.activePage == 'LoaderPage' && page.loadFileClick) {
|
||||
page.loadFileClick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadBufferToggle() {
|
||||
if (!this.loaderActive)
|
||||
this.loaderToggle();
|
||||
this.$nextTick(() => {
|
||||
const page = this.$refs.page;
|
||||
if (this.activePage == 'LoaderPage' && page.showPasteText) {
|
||||
page.showPasteText();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setPositionToggle() {
|
||||
this.setPositionActive = !this.setPositionActive;
|
||||
const page = this.$refs.page;
|
||||
@@ -773,6 +851,12 @@ class Reader {
|
||||
}
|
||||
}
|
||||
|
||||
clickControlToggle() {
|
||||
const newSettings = _.cloneDeep(this.settings);
|
||||
newSettings.clickControl = !this.clickControl;
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
|
||||
offlineModeToggle() {
|
||||
this.offlineModeActive = !this.offlineModeActive;
|
||||
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
||||
@@ -861,6 +945,9 @@ class Reader {
|
||||
|
||||
switch (action) {
|
||||
case 'loader':
|
||||
case 'loadFile':
|
||||
case 'loadBuffer':
|
||||
case 'help':
|
||||
case 'fullScreen':
|
||||
case 'setPosition':
|
||||
case 'search':
|
||||
@@ -870,6 +957,7 @@ class Reader {
|
||||
case 'contents':
|
||||
case 'libs':
|
||||
case 'recentBooks':
|
||||
case 'clickControl':
|
||||
case 'offlineMode':
|
||||
case 'settings':
|
||||
if (this.progressActive) {
|
||||
@@ -1106,7 +1194,9 @@ class Reader {
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
if (!this.showHelpOnErrorIfNeeded(e.message)) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
} finally {
|
||||
this.checkNewVersionAvailable();
|
||||
}
|
||||
@@ -1172,6 +1262,12 @@ class Reader {
|
||||
case 'loader':
|
||||
this.loaderToggle();
|
||||
break;
|
||||
case 'loadFile':
|
||||
this.loadFileToggle();
|
||||
break;
|
||||
case 'loadBuffer':
|
||||
this.loadBufferToggle();
|
||||
break;
|
||||
case 'help':
|
||||
this.helpToggle();
|
||||
break;
|
||||
@@ -1214,6 +1310,9 @@ class Reader {
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'clickControl':
|
||||
this.clickControlToggle();
|
||||
break;
|
||||
case 'offlineMode':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
@@ -1316,13 +1415,14 @@ class Reader {
|
||||
if (!result && event.type == 'keydown') {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
|
||||
if (action == 'loader') {
|
||||
/*if (action == 'loader') {
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
|
||||
if (!result && this.activePage == 'TextPage') {
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
}*/
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
Что нового:
|
||||
</template>
|
||||
|
||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||
<div style="line-height: 20px; min-width: 300px">
|
||||
<div v-html="whatsNewContent"></div>
|
||||
</div>
|
||||
|
||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||
<span slot="footer">
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||
|
||||
<template #footer>
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
|
||||
Больше не показывать
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog2" v-model="donationVisible">
|
||||
@@ -49,17 +54,40 @@
|
||||
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
|
||||
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
|
||||
Помочь проекту
|
||||
</q-btn>
|
||||
</q-btn-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<template #footer>
|
||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||
<br>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||
</span>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">
|
||||
Напомнить позже
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
||||
<template #header>
|
||||
Обнаружена невалидная ссылка в поле "URL книги".
|
||||
<br>
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
|
||||
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
|
||||
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||
|
||||
<br><br>
|
||||
Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||
Из буфера обмена
|
||||
</q-btn>
|
||||
на странице загрузки.
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,6 +116,7 @@ class ReaderDialogs {
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
urlHelpVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -112,9 +141,9 @@ class ReaderDialogs {
|
||||
const whatsNew = versionHistory[0];
|
||||
if (this.showWhatsNewDialog &&
|
||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||
whatsNew.header != this.whatsNewContentHash) {
|
||||
this.whatsNewHeader != this.whatsNewContentHash) {
|
||||
await utils.sleep(2000);
|
||||
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
||||
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
|
||||
this.whatsNewVisible = true;
|
||||
}
|
||||
}
|
||||
@@ -128,6 +157,14 @@ class ReaderDialogs {
|
||||
}
|
||||
}
|
||||
|
||||
async showUrlHelp() {
|
||||
this.urlHelpVisible = true;
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.urlHelpVisible = false;
|
||||
}
|
||||
|
||||
donationDialogDisable() {
|
||||
this.donationVisible = false;
|
||||
if (this.showDonationDialog2020) {
|
||||
@@ -160,8 +197,11 @@ class ReaderDialogs {
|
||||
|
||||
whatsNewDisable() {
|
||||
this.whatsNewVisible = false;
|
||||
const whatsNew = versionHistory[0];
|
||||
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
||||
this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
|
||||
}
|
||||
|
||||
get whatsNewHeader() {
|
||||
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
@@ -181,7 +221,7 @@ class ReaderDialogs {
|
||||
}
|
||||
|
||||
keyHook() {
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ class ServerStorage {
|
||||
newRecentPatch.rev++;
|
||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||
|
||||
let applyMod = this.cachedRecentMod.data;
|
||||
const applyMod = this.cachedRecentMod.data;
|
||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||
|
||||
@@ -627,7 +627,7 @@ class ServerStorage {
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
if (!recurse && itemKey) {
|
||||
this.savingRecent = false;
|
||||
this.saveRecent(itemKey, true);
|
||||
await this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<!--div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||
Показывать "Оплатим хостинг вместе"
|
||||
@@ -60,7 +60,7 @@
|
||||
Показывать уведомление "Оплатим хостинг вместе"
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
@@ -63,48 +63,6 @@ class BookManager {
|
||||
}
|
||||
|
||||
await this.cleanRecentBooks();
|
||||
|
||||
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||
{
|
||||
await this.convertFileToDiskPrefix();
|
||||
if (this.recentRev > 10)
|
||||
await bmRecentStoreOld.clear();
|
||||
}
|
||||
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
||||
if (this.recentLast) {
|
||||
this.recent[this.recentLast.key] = this.recentLast;
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
let key = null;
|
||||
const len = await bmRecentStoreOld.length();
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
key = await bmRecentStoreOld.key(i);
|
||||
if (key) {
|
||||
let r = await bmRecentStoreOld.getItem(key);
|
||||
if (_.isObject(r) && r.key) {
|
||||
this.recent[r.key] = r;
|
||||
}
|
||||
} else {
|
||||
await bmRecentStoreOld.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
//размножение для дебага
|
||||
/*if (key) {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const k = this.keyFromUrl(i.toString());
|
||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
||||
}
|
||||
}*/
|
||||
|
||||
await bmRecentStoreNew.setItem('recent', this.recent);
|
||||
this.recentRev = 1;
|
||||
await bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
@@ -374,7 +332,7 @@ class BookManager {
|
||||
//-- recent --------------------------------------------------------------
|
||||
async recentSetItem(item = null, skipCheck = false) {
|
||||
const rev = await bmRecentStoreNew.getItem('rev');
|
||||
if (rev != this.recentRev && !skipCheck) {
|
||||
if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
|
||||
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||
Object.assign(this.recent, newRecent);
|
||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||
@@ -455,33 +413,6 @@ class BookManager {
|
||||
return isDel;
|
||||
}
|
||||
|
||||
async convertFileToDiskPrefix() {
|
||||
let isConverted = false;
|
||||
|
||||
const newRecent = {};
|
||||
for (let key of Object.keys(this.recent)) {
|
||||
let newKey = key;
|
||||
let newUrl = this.recent[key].url;
|
||||
|
||||
if (newKey.indexOf('66696c65') == 0) {
|
||||
newKey = newKey.replace(/^66696c65/, '6469736b');
|
||||
if (newUrl)
|
||||
newUrl = newUrl.replace(/^file/, 'disk');
|
||||
isConverted = true;
|
||||
}
|
||||
|
||||
newRecent[newKey] = this.recent[key];
|
||||
newRecent[newKey].key = newKey;
|
||||
if (newUrl)
|
||||
newRecent[newKey].url = newUrl;
|
||||
}
|
||||
if (isConverted) {
|
||||
this.recent = newRecent;
|
||||
await this.recentSetItem(null, true);
|
||||
}
|
||||
return isConverted;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (this.recentLastKey) {
|
||||
return this.recent[this.recentLastKey];
|
||||
|
||||
@@ -1,62 +1,113 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
version: '0.11.4',
|
||||
releaseDate: '2022-04-14',
|
||||
showUntil: '2022-04-13',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>небольшие дополнения интерфейса</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.3',
|
||||
releaseDate: '2022-03-29',
|
||||
showUntil: '2022-03-28',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.2',
|
||||
releaseDate: '2022-01-11',
|
||||
showUntil: '2022-01-10',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.1',
|
||||
releaseDate: '2021-12-03',
|
||||
showUntil: '2021-12-02',
|
||||
header: '0.11.1 (2021-12-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на JembaDb вместо SQLite</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.0',
|
||||
releaseDate: '2021-11-18',
|
||||
showUntil: '2021-11-17',
|
||||
header: '0.11.0 (2021-11-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на Vue 3</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.3',
|
||||
releaseDate: '2021-10-24',
|
||||
showUntil: '2021-10-23',
|
||||
header: '0.10.3 (2021-10-24)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.2',
|
||||
releaseDate: '2021-10-19',
|
||||
showUntil: '2021-10-18',
|
||||
header: '0.10.2 (2021-10-19)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>актуализация версий пакетов и стека используемых технологий</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.1',
|
||||
releaseDate: '2021-10-10',
|
||||
showUntil: '2021-10-09',
|
||||
header: '0.10.1 (2021-10-10)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.0',
|
||||
releaseDate: '2021-02-09',
|
||||
showUntil: '2021-02-16',
|
||||
header: '0.10.0 (2021-02-09)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -65,12 +116,14 @@ export const versionHistory = [
|
||||
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||
<li>немного улучшен парсинг fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.12',
|
||||
releaseDate: '2020-12-18',
|
||||
showUntil: '2020-12-17',
|
||||
header: '0.9.12 (2020-12-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -79,23 +132,27 @@ export const versionHistory = [
|
||||
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||
<li>улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.11',
|
||||
releaseDate: '2020-12-09',
|
||||
showUntil: '2020-12-08',
|
||||
header: '0.9.11 (2020-12-09)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.10',
|
||||
releaseDate: '2020-12-03',
|
||||
showUntil: '2020-12-10',
|
||||
header: '0.9.10 (2020-12-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -103,69 +160,81 @@ export const versionHistory = [
|
||||
<li>добавлена поддержка Rar-архивов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.9',
|
||||
releaseDate: '2020-11-21',
|
||||
showUntil: '2020-11-20',
|
||||
header: '0.9.9 (2020-11-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.8',
|
||||
releaseDate: '2020-11-13',
|
||||
showUntil: '2020-11-12',
|
||||
header: '0.9.8 (2020-11-13)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено окно "Оглавление/закладки"</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.7',
|
||||
releaseDate: '2020-11-12',
|
||||
showUntil: '2020-11-11',
|
||||
header: '0.9.7 (2020-11-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.6',
|
||||
releaseDate: '2020-11-06',
|
||||
showUntil: '2020-11-05',
|
||||
header: '0.9.6 (2020-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>завершена работа над новым окном "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.5',
|
||||
releaseDate: '2020-11-01',
|
||||
showUntil: '2020-10-31',
|
||||
header: '0.9.5 (2020-11-01)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.4',
|
||||
releaseDate: '2020-10-29',
|
||||
showUntil: '2020-10-28',
|
||||
header: '0.9.4 (2020-10-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -173,23 +242,27 @@ export const versionHistory = [
|
||||
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.3',
|
||||
releaseDate: '2020-05-21',
|
||||
showUntil: '2020-05-20',
|
||||
header: '0.9.3 (2020-05-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.2',
|
||||
releaseDate: '2020-03-15',
|
||||
showUntil: '2020-04-25',
|
||||
header: '0.9.2 (2020-03-15)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -197,119 +270,139 @@ export const versionHistory = [
|
||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.1',
|
||||
releaseDate: '2020-03-03',
|
||||
showUntil: '2020-03-02',
|
||||
header: '0.9.1 (2020-03-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение работы серверной части</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.0',
|
||||
releaseDate: '2020-02-26',
|
||||
showUntil: '2020-02-25',
|
||||
header: '0.9.0 (2020-02-26)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на UI-фреймфорк Quasar</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.4',
|
||||
releaseDate: '2020-02-06',
|
||||
showUntil: '2020-02-05',
|
||||
header: '0.8.4 (2020-02-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен paypal-адрес для пожертвований</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.3',
|
||||
releaseDate: '2020-01-28',
|
||||
showUntil: '2020-01-27',
|
||||
header: '0.8.3 (2020-01-28)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.2',
|
||||
releaseDate: '2020-01-20',
|
||||
showUntil: '2020-01-19',
|
||||
header: '0.8.2 (2020-01-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.1',
|
||||
releaseDate: '2020-01-07',
|
||||
showUntil: '2020-01-06',
|
||||
header: '0.8.1 (2020-01-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата FB3</li>
|
||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.0',
|
||||
releaseDate: '2020-01-02',
|
||||
showUntil: '2020-01-05',
|
||||
header: '0.8.0 (2020-01-02)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>окончательный переход на https</li>
|
||||
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.9',
|
||||
releaseDate: '2019-11-27',
|
||||
showUntil: '2019-11-26',
|
||||
header: '0.7.9 (2019-11-27)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.8',
|
||||
releaseDate: '2019-11-25',
|
||||
showUntil: '2019-11-24',
|
||||
header: '0.7.8 (2019-11-25)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение html-фильтров для сайтов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.7',
|
||||
releaseDate: '2019-11-06',
|
||||
showUntil: '2019-11-10',
|
||||
header: '0.7.7 (2019-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -321,34 +414,40 @@ export const versionHistory = [
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.6',
|
||||
releaseDate: '2019-10-30',
|
||||
showUntil: '2019-10-29',
|
||||
header: '0.7.6 (2019-10-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.5',
|
||||
releaseDate: '2019-10-22',
|
||||
showUntil: '2019-10-21',
|
||||
header: '0.7.5 (2019-10-22)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.3',
|
||||
releaseDate: '2019-10-18',
|
||||
showUntil: '2019-10-17',
|
||||
header: '0.7.3 (2019-10-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -357,12 +456,14 @@ export const versionHistory = [
|
||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.1',
|
||||
releaseDate: '2019-09-20',
|
||||
showUntil: '2019-09-19',
|
||||
header: '0.7.1 (2019-09-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -370,12 +471,14 @@ export const versionHistory = [
|
||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||
<li>актуализирована справка</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.0',
|
||||
releaseDate: '2019-09-07',
|
||||
showUntil: '2019-10-01',
|
||||
header: '0.7.0 (2019-09-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -386,23 +489,27 @@ export const versionHistory = [
|
||||
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.10',
|
||||
releaseDate: '2019-07-21',
|
||||
showUntil: '2019-07-20',
|
||||
header: '0.6.10 (2019-07-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.9',
|
||||
releaseDate: '2019-06-23',
|
||||
showUntil: '2019-06-22',
|
||||
header: '0.6.9 (2019-06-23)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -413,12 +520,14 @@ export const versionHistory = [
|
||||
<li>улучшены прогрессбары</li>
|
||||
<li>исправления недочетов, небольшие оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.7',
|
||||
releaseDate: '2019-05-30',
|
||||
showUntil: '2019-06-05',
|
||||
header: '0.6.7 (2019-05-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -431,36 +540,42 @@ export const versionHistory = [
|
||||
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||
<li>исправления багов и недочетов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.6',
|
||||
releaseDate: '2019-03-29',
|
||||
showUntil: '2019-03-29',
|
||||
header: '0.6.6 (2019-03-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.4',
|
||||
releaseDate: '2019-03-24',
|
||||
showUntil: '2019-03-24',
|
||||
header: '0.6.4 (2019-03-24)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов, оптимизации</li>
|
||||
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.5.4',
|
||||
releaseDate: '2019-03-04',
|
||||
showUntil: '2019-03-04',
|
||||
header: '0.5.4 (2019-03-04)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -469,12 +584,14 @@ export const versionHistory = [
|
||||
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.3.0',
|
||||
releaseDate: '2019-02-17',
|
||||
showUntil: '2019-02-17',
|
||||
header: '0.3.0 (2019-02-17)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -482,12 +599,14 @@ export const versionHistory = [
|
||||
<li>улучшено распознавание текста</li>
|
||||
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.1.7',
|
||||
releaseDate: '2019-02-14',
|
||||
showUntil: '2019-02-14',
|
||||
header: '0.1.7 (2019-02-14)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -497,17 +616,20 @@ export const versionHistory = [
|
||||
<li>добавлена возможность сброса настроек</li>
|
||||
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.1.0',
|
||||
releaseDate: '2019-02-12',
|
||||
showUntil: '2019-02-12',
|
||||
header: '0.1.0 (2019-02-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@ const plugins = {
|
||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||
|
||||
//const q: {Quasar, QuasarOptions: { config, components, directives, plugins }};
|
||||
export default {
|
||||
quasar: Quasar,
|
||||
options: { config, components, directives, plugins },
|
||||
|
||||
@@ -2,8 +2,10 @@ import * as utils from '../../share/utils';
|
||||
import googleFonts from './fonts/fonts.json';
|
||||
|
||||
const readerActions = {
|
||||
'help': 'Вызвать cправку',
|
||||
'loader': 'На страницу загрузки',
|
||||
'loadFile': 'Загрузить файл с диска',
|
||||
'loadBuffer': 'Загрузить из буфера обмена',
|
||||
'help': 'Вызвать cправку',
|
||||
'settings': 'Настроить',
|
||||
'undoAction': 'Действие назад',
|
||||
'redoAction': 'Действие вперед',
|
||||
@@ -15,6 +17,7 @@ const readerActions = {
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'convOptions': 'Настроить конвертирование',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'clickControl': 'Управление кликом',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Сетевая библиотека',
|
||||
@@ -35,6 +38,9 @@ const readerActions = {
|
||||
|
||||
//readerActions[name]
|
||||
const toolButtons = [
|
||||
{name: 'loadFile', show: true},
|
||||
{name: 'loadBuffer', show: true},
|
||||
{name: 'help', show: true},
|
||||
{name: 'undoAction', show: true},
|
||||
{name: 'redoAction', show: true},
|
||||
{name: 'fullScreen', show: true},
|
||||
@@ -47,13 +53,16 @@ const toolButtons = [
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
{name: 'recentBooks', show: true},
|
||||
{name: 'clickControl', show: false},
|
||||
{name: 'offlineMode', show: false},
|
||||
];
|
||||
|
||||
//readerActions[name]
|
||||
const hotKeys = [
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{name: 'loader', codes: ['Escape']},
|
||||
{name: 'loadFile', codes: ['F3']},
|
||||
{name: 'loadBuffer', codes: ['F4']},
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{name: 'settings', codes: ['S']},
|
||||
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||
@@ -67,6 +76,7 @@ const hotKeys = [
|
||||
{name: 'contents', codes: ['C']},
|
||||
{name: 'libs', codes: ['L']},
|
||||
{name: 'recentBooks', codes: ['X']},
|
||||
{name: 'clickControl', codes: ['Ctrl+B']},
|
||||
{name: 'offlineMode', codes: ['O']},
|
||||
|
||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||
|
||||
@@ -35,6 +35,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
@@ -24,6 +24,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
1456
package-lock.json
generated
1456
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.11.1",
|
||||
"version": "0.11.4",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
@@ -10,8 +10,8 @@
|
||||
"scripts": {
|
||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -o dist/linux/liberama .",
|
||||
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -o dist/win/liberama .",
|
||||
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -C GZip -o dist/linux/liberama .",
|
||||
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -C GZip -o dist/win/liberama .",
|
||||
"lint": "eslint --ext=.js,.vue client server",
|
||||
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
||||
"postinstall": "npm run build:client-dev && node build/linux"
|
||||
@@ -35,6 +35,7 @@
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"mini-css-extract-plugin": "^2.4.4",
|
||||
"pkg": "^5.5.1",
|
||||
"terser-webpack-plugin": "^5.2.5",
|
||||
"vue-eslint-parser": "^8.0.1",
|
||||
"vue-loader": "^16.8.3",
|
||||
@@ -59,6 +60,7 @@
|
||||
"got": "^11.8.2",
|
||||
"he": "^1.2.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jembadb": "^2.3.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.5",
|
||||
@@ -66,7 +68,6 @@
|
||||
"pako": "^2.0.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pidusage": "^3.0.0",
|
||||
"pkg": "^4.4.9",
|
||||
"quasar": "^2.3.2",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"sanitize-html": "^2.5.3",
|
||||
|
||||
@@ -55,8 +55,7 @@ class WebSocketController {
|
||||
ws.lastActivity = Date.now();
|
||||
|
||||
//pong for WebSocketConnection
|
||||
if (req._rpo === 1)
|
||||
this.send({_rok: 1}, req, ws);
|
||||
this.send({_rok: 1}, req, ws);
|
||||
|
||||
switch (req.action) {
|
||||
case 'test':
|
||||
|
||||
@@ -5,26 +5,22 @@ const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtExcepti
|
||||
|
||||
//singleton
|
||||
class AsyncExit {
|
||||
constructor() {
|
||||
constructor(signals = exitSignals, codeOnSignal = 2) {
|
||||
if (!instance) {
|
||||
this.onSignalCallbacks = new Map();
|
||||
this.callbacks = new Map();
|
||||
this.afterCallbacks = new Map();
|
||||
this.exitTimeout = defaultTimeout;
|
||||
this.inited = false;
|
||||
|
||||
this._init(signals, codeOnSignal);
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
init(signals = null, codeOnSignal = 2) {
|
||||
if (this.inited)
|
||||
throw new Error('AsyncExit: initialized already');
|
||||
|
||||
if (!signals)
|
||||
signals = exitSignals;
|
||||
|
||||
_init(signals, codeOnSignal) {
|
||||
const runSingalCallbacks = async(signal) => {
|
||||
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
||||
try {
|
||||
@@ -41,8 +37,6 @@ class AsyncExit {
|
||||
this.exit(codeOnSignal);
|
||||
});
|
||||
}
|
||||
|
||||
this.inited = true;
|
||||
}
|
||||
|
||||
onSignal(signalCallback) {
|
||||
|
||||
@@ -9,8 +9,7 @@ const cleanPeriod = 5*1000;//5 секунд
|
||||
class WebSocketConnection {
|
||||
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
||||
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
|
||||
//const ws = 'ws';//for nodejs
|
||||
this.WebSocket = (isBrowser ? WebSocket : null/*for nodejs require(ws)*/);
|
||||
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.listeners = [];
|
||||
@@ -166,7 +165,7 @@ class WebSocketConnection {
|
||||
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
|
||||
const requestId = this.requestId;//реентерабельность!!!
|
||||
|
||||
this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1
|
||||
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||
|
||||
let resp = {};
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
|
||||
const ayncExit = new (require('../core/AsyncExit'))();//singleton
|
||||
const { JembaDb, JembaDbThread } = require('./JembaDb');
|
||||
const { JembaDb, JembaDbThread } = require('jembadb');
|
||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||
|
||||
const jembaMigrations = require('./jembaMigrations');
|
||||
@@ -14,6 +14,7 @@ class JembaConnManager {
|
||||
constructor() {
|
||||
if (!instance) {
|
||||
this.inited = false;
|
||||
this._db = {};
|
||||
|
||||
instance = this;
|
||||
}
|
||||
@@ -28,6 +29,8 @@ class JembaConnManager {
|
||||
this.config = config;
|
||||
this._db = {};
|
||||
|
||||
ayncExit.add(this.close.bind(this));
|
||||
|
||||
for (const dbConfig of this.config.jembaDb) {
|
||||
const dbPath = `${this.config.dataDir}/db/${dbConfig.dbName}`;
|
||||
|
||||
@@ -44,11 +47,23 @@ class JembaConnManager {
|
||||
} else {
|
||||
dbConn = new JembaDb();
|
||||
}
|
||||
this._db[dbConfig.dbName] = dbConn;
|
||||
|
||||
log(`Open "${dbConfig.dbName}" begin`);
|
||||
await dbConn.openDb({dbPath, cacheSize: dbConfig.cacheSize, compressed: dbConfig.compressed, forceFileClosing: dbConfig.forceFileClosing});
|
||||
await dbConn.lock({
|
||||
dbPath,
|
||||
create: true,
|
||||
softLock: true,
|
||||
|
||||
if (dbConfig.openAll) {
|
||||
tableDefaults: {
|
||||
cacheSize: dbConfig.cacheSize,
|
||||
compressed: dbConfig.compressed,
|
||||
forceFileClosing: dbConfig.forceFileClosing,
|
||||
typeCompatMode: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dbConfig.openAll || forceAutoRepair || dbConfig.autoRepair) {
|
||||
try {
|
||||
await dbConn.openAll();
|
||||
} catch(e) {
|
||||
@@ -77,21 +92,15 @@ class JembaConnManager {
|
||||
if (applied.length)
|
||||
log(`${applied.length} migrations applied to "${dbConfig.dbName}"`);
|
||||
}
|
||||
|
||||
this._db[dbConfig.dbName] = dbConn;
|
||||
}
|
||||
|
||||
ayncExit.add(this.close.bind(this));
|
||||
|
||||
this.inited = true;
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.inited)
|
||||
return;
|
||||
|
||||
for (const dbConfig of this.config.jembaDb) {
|
||||
await this._db[dbConfig.dbName].closeDb();
|
||||
if (this._db[dbConfig.dbName])
|
||||
await this._db[dbConfig.dbName].unlock();
|
||||
}
|
||||
|
||||
this._db = {};
|
||||
|
||||
@@ -1,536 +0,0 @@
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const Table = require('./Table');
|
||||
const utils = require('./utils');
|
||||
|
||||
/* API methods:
|
||||
openDb
|
||||
closeDb
|
||||
|
||||
create
|
||||
drop
|
||||
|
||||
open
|
||||
openAll
|
||||
close
|
||||
closeAll
|
||||
|
||||
tableExists
|
||||
getDbInfo
|
||||
getDbSize
|
||||
|
||||
select
|
||||
insert
|
||||
update
|
||||
delete
|
||||
|
||||
esc
|
||||
*/
|
||||
|
||||
class JembaDb {
|
||||
constructor() {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
dbPath: String,
|
||||
//table open defaults
|
||||
inMemory: Boolean, false
|
||||
cacheSize: Number, 5
|
||||
compressed: Number, {0..9}, 0
|
||||
recreate: Boolean, false,
|
||||
autoRepair: Boolean, false,
|
||||
forceFileClosing: Boolean, false,
|
||||
lazyOpen: Boolean, false,
|
||||
}
|
||||
*/
|
||||
async openDb(query = {}) {
|
||||
if (this.opened)
|
||||
throw new Error(`Database ${this.dbPath} has already been opened`);
|
||||
|
||||
if (!query.dbPath)
|
||||
throw new Error(`'query.dbPath' parameter is required`);
|
||||
|
||||
this.dbPath = query.dbPath;
|
||||
await fs.mkdir(this.dbPath, { recursive: true });
|
||||
|
||||
this.table = new Map();
|
||||
this.tableOpenDefaults = {
|
||||
inMemory: query.inMemory,
|
||||
cacheSize: query.cacheSize,
|
||||
compressed: query.compressed,
|
||||
recreate: query.recreate,
|
||||
autoRepair: query.autoRepair,
|
||||
forceFileClosing: query.forceFileClosing,
|
||||
lazyOpen: query.lazyOpen,
|
||||
};
|
||||
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
async closeDb() {
|
||||
if (!this.opened)
|
||||
return;
|
||||
|
||||
await this.closeAll();
|
||||
this.opened = false;
|
||||
|
||||
//console.log('closed');
|
||||
}
|
||||
|
||||
checkOpened() {
|
||||
if (!this.opened)
|
||||
throw new Error('Database closed');
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
table: 'tableName',
|
||||
quietIfExists: Boolean,
|
||||
inMemory: Boolean, false
|
||||
cacheSize: Number, 5
|
||||
compressed: Number, {0..9}, 0
|
||||
recreate: Boolean, false,
|
||||
autoRepair: Boolean, false,
|
||||
forceFileClosing: Boolean, false,
|
||||
lazyOpen: Boolean, false,
|
||||
|
||||
in: 'tableName',
|
||||
flag: Object || Array, {name: 'flag1', check: '(r) => r.id > 10'}
|
||||
hash: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
|
||||
index: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
|
||||
}
|
||||
result = {}
|
||||
*/
|
||||
async create(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if ((!query.table && !query.in) || (query.table && query.in))
|
||||
throw new Error(`One of 'query.table' or 'query.in' parameters is required, but not both`);
|
||||
|
||||
let table;
|
||||
if (query.table) {
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
if (!query.quietIfExists)
|
||||
throw new Error(`Table '${query.table}' already exists`);
|
||||
|
||||
table = this.table.get(query.table);
|
||||
} else {
|
||||
table = new Table();
|
||||
this.table.set(query.table, table);
|
||||
|
||||
await this.open(query);
|
||||
}
|
||||
} else {
|
||||
if (await this.tableExists({table: query.in})) {
|
||||
table = this.table.get(query.in);
|
||||
} else {
|
||||
throw new Error(`Table '${query.in}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.flag || query.hash || query.index) {
|
||||
await table.create({
|
||||
quietIfExists: query.quietIfExists,
|
||||
flag: query.flag,
|
||||
hash: query.hash,
|
||||
index: query.index,
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
table: 'tableName',
|
||||
|
||||
in: 'tableName',
|
||||
flag: Object || Array, {name: 'flag1'}
|
||||
hash: Object || Array, {field: 'field1'}
|
||||
index: Object || Array, {field: 'field1'}
|
||||
}
|
||||
result = {}
|
||||
*/
|
||||
async drop(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if ((!query.table && !query.in) || (query.table && query.in))
|
||||
throw new Error(`One of 'query.table' or 'query.in' parameters is required, but not both`);
|
||||
|
||||
if (query.table) {
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
const table = this.table.get(query.table);
|
||||
if (table && table.opened) {
|
||||
await table.close();
|
||||
}
|
||||
|
||||
const basePath = `${this.dbPath}/${query.table}`;
|
||||
await fs.rmdir(basePath, { recursive: true });
|
||||
|
||||
this.table.delete(query.table);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
} else {
|
||||
if (await this.tableExists({table: query.in})) {
|
||||
const table = this.table.get(query.in);
|
||||
|
||||
if (table) {
|
||||
if (query.flag || query.hash || query.index) {
|
||||
await table.drop({
|
||||
flag: query.flag,
|
||||
hash: query.hash,
|
||||
index: query.index,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Table '${query.in}' has not been opened yet`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Table '${query.in}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName',
|
||||
inMemory: Boolean, false
|
||||
cacheSize: Number, 5
|
||||
compressed: Number, {0..9}, 0
|
||||
recreate: Boolean, false,
|
||||
autoRepair: Boolean, false,
|
||||
forceFileClosing: Boolean, false,
|
||||
lazyOpen: Boolean, false,
|
||||
}
|
||||
*/
|
||||
async open(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
let table = this.table.get(query.table);
|
||||
|
||||
if (!table) {
|
||||
table = new Table();
|
||||
}
|
||||
|
||||
if (!table.opened) {
|
||||
const opts = Object.assign({}, this.tableOpenDefaults, query);
|
||||
opts.tablePath = `${this.dbPath}/${query.table}`;
|
||||
await table.open(opts);
|
||||
}
|
||||
|
||||
this.table.set(query.table, table);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async _getTableList() {
|
||||
const result = [];
|
||||
const files = await fs.readdir(this.dbPath, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
if (file.isDirectory()) {
|
||||
if (file.name.indexOf('___temporary_recreating') >= 0)
|
||||
continue;
|
||||
result.push(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
inMemory: Boolean, false
|
||||
cacheSize: Number, 5
|
||||
compressed: Number, {0..9}, 0
|
||||
recreate: Boolean, false,
|
||||
autoRepair: Boolean, false,
|
||||
forceFileClosing: Boolean, false,
|
||||
lazyOpen: Boolean, false,
|
||||
}
|
||||
*/
|
||||
async openAll(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
const tables = await this._getTableList();
|
||||
|
||||
//sequentially
|
||||
for (const table of tables) {
|
||||
this.checkOpened();
|
||||
await this.open(Object.assign({}, query, {table}));
|
||||
}
|
||||
|
||||
/*const promises = [];
|
||||
for (const table of tables) {
|
||||
promises.push(this.open(Object.assign({}, query, {table})));
|
||||
}
|
||||
await Promise.all(promises);*/
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName',
|
||||
}
|
||||
*/
|
||||
async close(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
let table = this.table.get(query.table);
|
||||
|
||||
if (table) {
|
||||
await table.close();
|
||||
}
|
||||
|
||||
this.table.delete(query.table);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
async closeAll() {
|
||||
this.checkOpened();
|
||||
|
||||
const promises = [];
|
||||
for (const table of this.table.keys()) {
|
||||
promises.push(this.close({table}));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName'
|
||||
},
|
||||
result = Boolean
|
||||
*/
|
||||
async tableExists(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
if (this.table.has(query.table))
|
||||
return true;
|
||||
|
||||
if (await utils.pathExists(`${this.dbPath}/${query.table}`))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
table: 'tableName'
|
||||
},
|
||||
result = {
|
||||
dbPath: String,
|
||||
tableName1: {opened: Boolean, ...},
|
||||
tableName2: {opened: Boolean, ...},
|
||||
...
|
||||
}
|
||||
*/
|
||||
async getDbInfo(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
const tables = await this._getTableList();
|
||||
|
||||
const result = {dbPath: this.dbPath};
|
||||
for (const table of tables) {
|
||||
if (!query.table || (query.table && table == query.table)) {
|
||||
const tableInstance = this.table.get(table);
|
||||
if (tableInstance && tableInstance.opened) {
|
||||
result[table] = await tableInstance.getMeta();
|
||||
result[table].opened = true;
|
||||
} else {
|
||||
result[table] = {opened: false};
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
result = {
|
||||
total: Number,
|
||||
tables: {
|
||||
tableName1: Number,
|
||||
tableName2: Number,
|
||||
...
|
||||
}
|
||||
}
|
||||
*/
|
||||
async getDbSize() {
|
||||
this.checkOpened();
|
||||
|
||||
const dirs = await fs.readdir(this.dbPath, { withFileTypes: true });
|
||||
|
||||
const result = {total: 0, tables: {}};
|
||||
for (const dir of dirs) {
|
||||
if (dir.isDirectory()) {
|
||||
const table = dir.name;
|
||||
const tablePath = `${this.dbPath}/${table}`;
|
||||
const files = await fs.readdir(tablePath, { withFileTypes: true });
|
||||
|
||||
if (!result.tables[table])
|
||||
result.tables[table] = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.isFile()) {
|
||||
let size = 0;
|
||||
try {
|
||||
size = (await fs.stat(`${tablePath}/${file.name}`)).size;
|
||||
} catch(e) {
|
||||
//
|
||||
}
|
||||
result.tables[table] += size;
|
||||
result.total += size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName',
|
||||
distinct: 'fieldName' || Array,
|
||||
count: Boolean,
|
||||
map: '(r) => ({id1: r.id, ...})',
|
||||
where: `@@index('field1', 10, 20)`,
|
||||
sort: '(a, b) => a.id - b.id',
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
}
|
||||
result = Array
|
||||
*/
|
||||
async select(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
const table = this.table.get(query.table);
|
||||
if (table) {
|
||||
return await table.select(query);
|
||||
} else {
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
throw new Error(`Table '${query.table}' has not been opened yet`);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName',
|
||||
replace: Boolean,
|
||||
(!) rows: Array,
|
||||
}
|
||||
result = {
|
||||
(!) inserted: Number,
|
||||
(!) replaced: Number,
|
||||
}
|
||||
*/
|
||||
async insert(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
const table = this.table.get(query.table);
|
||||
if (table) {
|
||||
return await table.insert(query);
|
||||
} else {
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
throw new Error(`Table '${query.table}' has not been opened yet`);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName',
|
||||
(!) mod: '(r) => r.count++',
|
||||
where: `@@index('field1', 10, 20)`,
|
||||
sort: '(a, b) => a.id - b.id',
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
}
|
||||
result = {
|
||||
(!) updated: Number,
|
||||
}
|
||||
*/
|
||||
async update(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
const table = this.table.get(query.table);
|
||||
if (table) {
|
||||
return await table.update(query);
|
||||
} else {
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
throw new Error(`Table '${query.table}' has not been opened yet`);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) table: 'tableName',
|
||||
where: `@@index('field1', 10, 20)`,
|
||||
sort: '(a, b) => a.id - b.id',
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
}
|
||||
result = {
|
||||
(!) deleted: Number,
|
||||
}
|
||||
*/
|
||||
async delete(query = {}) {
|
||||
this.checkOpened();
|
||||
|
||||
if (!query.table)
|
||||
throw new Error(`'query.table' parameter is required`);
|
||||
|
||||
const table = this.table.get(query.table);
|
||||
if (table) {
|
||||
return await table.delete(query);
|
||||
} else {
|
||||
if (await this.tableExists({table: query.table})) {
|
||||
throw new Error(`Table '${query.table}' has not been opened yet`);
|
||||
} else {
|
||||
throw new Error(`Table '${query.table}' does not exist`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esc(obj) {
|
||||
return utils.esc(obj);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JembaDb;
|
||||
@@ -1,49 +0,0 @@
|
||||
const { parentPort } = require('worker_threads');
|
||||
|
||||
const JembaDb = require('./JembaDb');
|
||||
|
||||
const db = new JembaDb();
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', async(mes) => {
|
||||
let result = {};
|
||||
try {
|
||||
if (db[mes.action])
|
||||
result.result = await db[mes.action](mes.query);
|
||||
else
|
||||
result = {error: 'Action not found: ' + mes.action};
|
||||
} catch (e) {
|
||||
result = {error: e.message};
|
||||
}
|
||||
|
||||
result.requestId = mes.requestId;
|
||||
parentPort.postMessage(result);
|
||||
});
|
||||
}
|
||||
|
||||
//This is for proper working of pkg (by zeit) and worker_threads
|
||||
//just a copy of the above code as a string
|
||||
module.exports = `
|
||||
const { parentPort } = require('worker_threads');
|
||||
|
||||
const JembaDb = require('./JembaDb');
|
||||
|
||||
const db = new JembaDb();
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', async(mes) => {
|
||||
let result = {};
|
||||
try {
|
||||
if (db[mes.action])
|
||||
result.result = await db[mes.action](mes.query);
|
||||
else
|
||||
result = {error: 'Action not found: ' + mes.action};
|
||||
} catch (e) {
|
||||
result = {error: e.message};
|
||||
}
|
||||
|
||||
result.requestId = mes.requestId;
|
||||
parentPort.postMessage(result);
|
||||
});
|
||||
}
|
||||
`.replace('./JembaDb', `${__dirname.replace(/\\/g, '/')}/JembaDb`);
|
||||
@@ -1,119 +0,0 @@
|
||||
const { Worker } = require('worker_threads');
|
||||
const utils = require('./utils');
|
||||
const JembaDbChild = require('./JembaDbChild');
|
||||
/* API methods:
|
||||
openDb
|
||||
closeDb
|
||||
|
||||
create
|
||||
drop
|
||||
|
||||
open
|
||||
openAll
|
||||
close
|
||||
closeAll
|
||||
|
||||
tableExists
|
||||
getInfo
|
||||
getDbSize
|
||||
|
||||
select
|
||||
insert
|
||||
update
|
||||
delete
|
||||
|
||||
esc
|
||||
*/
|
||||
|
||||
class JembaDbThread {
|
||||
constructor() {
|
||||
this.worker = null;
|
||||
this.listeners = new Map();
|
||||
this.requestId = 0;
|
||||
|
||||
const apiMethods = [
|
||||
'create', 'drop', 'open', 'openAll', 'close', 'closeAll',
|
||||
'tableExists', 'getDbInfo', 'getDbSize', 'select', 'insert', 'update', 'delete', 'dumpTables'
|
||||
];
|
||||
|
||||
for (const action of apiMethods) {
|
||||
this[action] = async(query) => this._action(action, query);
|
||||
}
|
||||
}
|
||||
|
||||
_terminate() {
|
||||
if (this.worker) {
|
||||
for (const listener of this.listeners.values()) {
|
||||
listener({error: 'Worker terminated'});
|
||||
}
|
||||
this.worker.terminate();
|
||||
}
|
||||
this.worker = null;
|
||||
}
|
||||
|
||||
_runWoker() {
|
||||
//const worker = new Worker(`${__dirname}/JembaDbChild.js`);
|
||||
const worker = new Worker(JembaDbChild, {eval: true});
|
||||
|
||||
worker.on('message', (mes) => {
|
||||
const listener = this.listeners.get(mes.requestId);
|
||||
if (listener)
|
||||
listener(mes);
|
||||
});
|
||||
|
||||
worker.on('error', (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
worker.on('exit', () => {
|
||||
this._terminate();
|
||||
});
|
||||
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
_action(action, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.requestId++;
|
||||
|
||||
const requestId = this.requestId; //!!!
|
||||
this.listeners.set(requestId, (mes) => {
|
||||
this.listeners.delete(requestId);
|
||||
|
||||
if (mes.error)
|
||||
reject(new Error(mes.error));
|
||||
else
|
||||
resolve(mes.result);
|
||||
});
|
||||
|
||||
if (this.worker) {
|
||||
this.worker.postMessage({requestId: this.requestId, action, query});
|
||||
} else {
|
||||
reject(new Error('Worker does not exist (database closed?)'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async openDb(query = {}) {
|
||||
if (!this.worker) {
|
||||
this._runWoker();
|
||||
} else {
|
||||
throw new Error('Worker has been created already');
|
||||
}
|
||||
|
||||
return this._action('openDb', query);
|
||||
}
|
||||
|
||||
async closeDb() {
|
||||
const result = await this._action('closeDb');
|
||||
this._terminate();
|
||||
//console.log('DB closed');
|
||||
return result;
|
||||
}
|
||||
|
||||
esc(obj) {
|
||||
return utils.esc(obj);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JembaDbThread;
|
||||
@@ -1,38 +0,0 @@
|
||||
class LockQueue {
|
||||
constructor(queueSize) {
|
||||
this.queueSize = queueSize;
|
||||
this.freed = true;
|
||||
this.waitingQueue = [];
|
||||
}
|
||||
|
||||
ret() {
|
||||
this.freed = true;
|
||||
if (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().onFreed();
|
||||
}
|
||||
}
|
||||
|
||||
get(take = true) {
|
||||
return new Promise((resolve) => {
|
||||
if (this.freed) {
|
||||
if (take)
|
||||
this.freed = false;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.waitingQueue.length >= this.queueSize)
|
||||
throw new Error('Lock queue is too long');
|
||||
|
||||
this.waitingQueue.push({
|
||||
onFreed: () => {
|
||||
if (take)
|
||||
this.freed = false;
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LockQueue;
|
||||
@@ -1,852 +0,0 @@
|
||||
const fs = require('fs').promises;
|
||||
const utils = require('./utils');
|
||||
|
||||
const TableReducer = require('./TableReducer');
|
||||
const TableRowsMem = require('./TableRowsMem');
|
||||
const TableRowsFile = require('./TableRowsFile');
|
||||
const LockQueue = require('./LockQueue');
|
||||
|
||||
const maxChangesLength = 10;
|
||||
|
||||
class Table {
|
||||
constructor() {
|
||||
this.rowsInterface = new TableRowsMem();
|
||||
|
||||
this.autoIncrement = 0;
|
||||
this.fileError = '';
|
||||
|
||||
this.openingLock = new LockQueue(100);
|
||||
this.lock = new LockQueue(100);
|
||||
|
||||
this.opened = false;
|
||||
this.closed = false;
|
||||
this.deltaStep = 0;
|
||||
this.changes = [];
|
||||
|
||||
//table options defaults
|
||||
this.inMemory = false;
|
||||
this.compressed = 0;
|
||||
this.cacheSize = 5;
|
||||
this.compressed = 0;
|
||||
this.recreate = false;
|
||||
this.autoRepair = false;
|
||||
this.forceFileClosing = false;
|
||||
}
|
||||
|
||||
checkErrors() {
|
||||
if (this.fileError)
|
||||
throw new Error(this.fileError);
|
||||
|
||||
if (this.closed)
|
||||
throw new Error('Table closed');
|
||||
|
||||
if (!this.opened)
|
||||
throw new Error('Table has not been opened yet');
|
||||
}
|
||||
|
||||
async waitForSaveChanges() {
|
||||
if (this.changes.length > maxChangesLength) {
|
||||
let i = this.changes.length - maxChangesLength;
|
||||
while (i > 0 && this.changes.length > maxChangesLength) {
|
||||
i--;
|
||||
await utils.sleep(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async recreateTable() {
|
||||
const tempTablePath = `${this.tablePath}___temporary_recreating`;
|
||||
await fs.rmdir(tempTablePath, { recursive: true });
|
||||
await fs.mkdir(tempTablePath, { recursive: true });
|
||||
|
||||
const tableRowsFileSrc = new TableRowsFile(this.tablePath, this.cacheSize);
|
||||
|
||||
const tableRowsFileDest = new TableRowsFile(tempTablePath, this.cacheSize, this.compressed);
|
||||
const reducerDest = new TableReducer(false, tempTablePath, this.compressed, tableRowsFileDest);
|
||||
|
||||
try {
|
||||
await tableRowsFileSrc.loadCorrupted();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
await reducerDest._load(true, `${this.tablePath}/meta.0`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const putRows = async(rows) => {
|
||||
const oldRows = [];
|
||||
const newRows = [];
|
||||
const newRowsStr = [];
|
||||
//checks
|
||||
for (const row of rows) {
|
||||
if (!row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const t = typeof(row.id);
|
||||
if (t !== 'number' && t !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldRow = await tableRowsFileDest.getRow(row.id);
|
||||
|
||||
if (oldRow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let str = '';
|
||||
try {
|
||||
str = JSON.stringify(row);//because of stringify errors
|
||||
} catch(e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newRows.push(row);
|
||||
oldRows.push({});
|
||||
newRowsStr.push(str);
|
||||
}
|
||||
|
||||
try {
|
||||
//reducer
|
||||
reducerDest._update(oldRows, newRows, 1);
|
||||
|
||||
//insert
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
const newRow = newRows[i];
|
||||
const newRowStr = newRowsStr[i];
|
||||
|
||||
tableRowsFileDest.setRow(newRow.id, newRow, newRowStr, 1);
|
||||
}
|
||||
|
||||
await tableRowsFileDest.saveDelta(1);
|
||||
await reducerDest._saveDelta(1);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
let rows = [];
|
||||
for (const id of tableRowsFileSrc.getAllIds()) {
|
||||
if (this.closed)
|
||||
throw new Error('Table closed');
|
||||
|
||||
let row = null;
|
||||
try {
|
||||
row = await tableRowsFileSrc.getRow(id);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push(row);
|
||||
if (rows.length > 1000) {
|
||||
await putRows(rows);
|
||||
rows = [];
|
||||
}
|
||||
}
|
||||
if (rows.length)
|
||||
await putRows(rows);
|
||||
|
||||
await tableRowsFileDest.saveDelta(0);
|
||||
|
||||
const delta = reducerDest._getDelta(0);
|
||||
delta.dumpMeta = true;
|
||||
await reducerDest._saveDelta(0);
|
||||
|
||||
await tableRowsFileSrc.destroy();
|
||||
await reducerDest._destroy();
|
||||
await tableRowsFileDest.destroy();
|
||||
|
||||
await fs.writeFile(`${tempTablePath}/state`, '1');
|
||||
|
||||
await fs.rmdir(this.tablePath, { recursive: true });
|
||||
await fs.rename(tempTablePath, this.tablePath);
|
||||
}
|
||||
|
||||
/*
|
||||
query: {
|
||||
tablePath: String,
|
||||
inMemory: Boolean,
|
||||
cacheSize: Number,
|
||||
compressed: Number, 0..9
|
||||
recreate: Boolean, false,
|
||||
autoRepair: Boolean, false,
|
||||
forceFileClosing: Boolean, false,
|
||||
lazyOpen: Boolean, false,
|
||||
}
|
||||
*/
|
||||
async _open(query = {}) {
|
||||
if (this.opening)
|
||||
return;
|
||||
this.opening = true;
|
||||
await this.openingLock.get();
|
||||
//console.log(query);
|
||||
try {
|
||||
if (this.opened)
|
||||
throw new Error('Table has already been opened');
|
||||
if (this.closed)
|
||||
throw new Error('Table instance has been destroyed. Please create a new one.');
|
||||
|
||||
this.inMemory = !!query.inMemory;
|
||||
|
||||
if (this.inMemory) {
|
||||
this.reducer = new TableReducer(this.inMemory, '', 0, this.rowsInterface);
|
||||
} else {
|
||||
if (!query.tablePath)
|
||||
throw new Error(`'query.tablePath' parameter is required`);
|
||||
|
||||
this.tablePath = query.tablePath;
|
||||
this.cacheSize = query.cacheSize || 5;
|
||||
this.compressed = query.compressed || 0;
|
||||
this.recreate = query.recreate || false;
|
||||
this.autoRepair = query.autoRepair || false;
|
||||
this.forceFileClosing = query.forceFileClosing || false;
|
||||
|
||||
await fs.mkdir(this.tablePath, { recursive: true });
|
||||
|
||||
this.tableRowsFile = new TableRowsFile(query.tablePath, this.cacheSize, this.compressed);
|
||||
this.rowsInterface = this.tableRowsFile;
|
||||
|
||||
this.reducer = new TableReducer(this.inMemory, this.tablePath, this.compressed, this.rowsInterface);
|
||||
|
||||
const statePath = `${this.tablePath}/state`;
|
||||
let state = null;
|
||||
if (await utils.pathExists(statePath)) {
|
||||
state = await fs.readFile(statePath, 'utf8');
|
||||
}
|
||||
|
||||
if (state === null) {//check if other files exists
|
||||
const files = await fs.readdir(this.tablePath);
|
||||
if (files.length)
|
||||
state = '0';
|
||||
}
|
||||
|
||||
if (this.recreate) {
|
||||
await this.recreateTable();
|
||||
state = '1';
|
||||
}
|
||||
|
||||
if (state !== null) {
|
||||
try {
|
||||
if (state === '1') {
|
||||
// load tableRowsFile & reducer
|
||||
this.autoIncrement = await this.tableRowsFile.load();
|
||||
await this.reducer._load();
|
||||
} else {
|
||||
throw new Error('Table corrupted')
|
||||
}
|
||||
} catch(e) {
|
||||
if (this.autoRepair) {
|
||||
console.error(e.message);
|
||||
await this.recreateTable();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
// load tableRowsFile & reducer
|
||||
this.autoIncrement = await this.tableRowsFile.load();
|
||||
await this.reducer._load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.opened = true;
|
||||
} catch(e) {
|
||||
await this.close();
|
||||
const errMes = `Open table (${query.tablePath}): ${e.message}`;
|
||||
if (!query.lazyOpen)
|
||||
throw new Error(errMes);
|
||||
else
|
||||
this.fileError = errMes;
|
||||
} finally {
|
||||
this.openingLock.ret();
|
||||
this.opening = false;
|
||||
}
|
||||
}
|
||||
|
||||
async open(query = {}) {
|
||||
if (query.lazyOpen) {
|
||||
this._open(query);
|
||||
} else {
|
||||
await this._open(query);
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.closed)
|
||||
return;
|
||||
|
||||
this.opened = false;
|
||||
this.closed = true;
|
||||
|
||||
if (!this.inMemory) {
|
||||
while (this.savingChanges) {
|
||||
await utils.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
//for GC
|
||||
if (this.reducer)
|
||||
await this.reducer._destroy();
|
||||
this.reducer = null;
|
||||
|
||||
if (this.rowsInterface)
|
||||
await this.rowsInterface.destroy();
|
||||
this.rowsInterface = null;
|
||||
this.tableRowsFile = null;
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
quietIfExists: Boolean,
|
||||
flag: Object || Array, {name: 'flag1', check: '(r) => r.id > 10'}
|
||||
hash: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
|
||||
index: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
|
||||
}
|
||||
result = {}
|
||||
*/
|
||||
async create(query) {
|
||||
await this.openingLock.get(false);
|
||||
this.checkErrors();
|
||||
|
||||
await this.lock.get();
|
||||
try {
|
||||
this.deltaStep++;
|
||||
try {
|
||||
if (query.flag) {
|
||||
for (const flag of utils.paramToArray(query.flag)) {
|
||||
await this.reducer._addFlag(flag, query.quietIfExists, this.deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.hash) {
|
||||
for (const hash of utils.paramToArray(query.hash)) {
|
||||
await this.reducer._addHash(hash, query.quietIfExists, this.deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.index) {
|
||||
for (const index of utils.paramToArray(query.index)) {
|
||||
await this.reducer._addIndex(index, query.quietIfExists, this.deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
this.changes.push([this.deltaStep, 1]);
|
||||
} catch(e) {
|
||||
this.changes.push([this.deltaStep, 0]);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return {};
|
||||
} finally {
|
||||
this.saveChanges();//no await
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
flag: Object || Array, {name: 'flag1'}
|
||||
hash: Object || Array, {field: 'field1'}
|
||||
index: Object || Array, {field: 'field1'}
|
||||
}
|
||||
result = {}
|
||||
*/
|
||||
async drop(query) {
|
||||
await this.openingLock.get(false);
|
||||
this.checkErrors();
|
||||
|
||||
await this.lock.get();
|
||||
try {
|
||||
this.deltaStep++;
|
||||
try {
|
||||
if (query.flag) {
|
||||
for (const flag of utils.paramToArray(query.flag)) {
|
||||
await this.reducer._delFlag(flag.name, this.deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.hash) {
|
||||
for (const hash of utils.paramToArray(query.hash)) {
|
||||
await this.reducer._delHash(hash.field, this.deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.index) {
|
||||
for (const index of utils.paramToArray(query.index)) {
|
||||
await this.reducer._delIndex(index.field, this.deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
this.changes.push([this.deltaStep, 1]);
|
||||
} catch(e) {
|
||||
this.changes.push([this.deltaStep, 0]);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return {};
|
||||
} finally {
|
||||
this.saveChanges();//no await
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
result = {
|
||||
inMemory: Boolean,
|
||||
flag: Array, [{name: 'flag1', check: '(r) => r.id > 10'}, ...]
|
||||
hash: Array, [{field: 'field1', type: 'string', depth: 11, allowUndef: false}, ...]
|
||||
index: Array, [{field: 'field1', type: 'string', depth: 11, allowUndef: false}, ...]
|
||||
}
|
||||
*/
|
||||
async getMeta() {
|
||||
this.checkErrors();
|
||||
|
||||
return {
|
||||
inMemory: this.inMemory,
|
||||
flag: this.reducer._listFlag(),
|
||||
hash: this.reducer._listHash(),
|
||||
index: this.reducer._listIndex(),
|
||||
};
|
||||
}
|
||||
|
||||
prepareWhere(where) {
|
||||
if (typeof(where) !== 'string')
|
||||
throw new Error('query.where must be a string');
|
||||
|
||||
return `async(__tr) => {${where.replace(/@@/g, 'return await __tr.').replace(/@/g, 'await __tr.')}}`;
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
distinct: 'fieldName' || Array,
|
||||
count: Boolean,
|
||||
map: '(r) => ({id1: r.id, ...})',
|
||||
where: `@@index('field1', 10, 20)`,
|
||||
sort: '(a, b) => a.id - b.id',
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
}
|
||||
result = Array
|
||||
*/
|
||||
async select(query = {}) {
|
||||
await this.openingLock.get(false);
|
||||
this.checkErrors();
|
||||
|
||||
let ids;//iterator
|
||||
if (query.where) {
|
||||
const where = this.prepareWhere(query.where);
|
||||
const whereFunc = new Function(`return ${where}`)();
|
||||
|
||||
ids = await whereFunc(this.reducer);
|
||||
} else {
|
||||
ids = this.rowsInterface.getAllIds();
|
||||
}
|
||||
|
||||
let found = [];
|
||||
|
||||
let distinct = () => true;
|
||||
if (query.distinct) {
|
||||
const distFields = (Array.isArray(query.distinct) ? query.distinct : [query.distinct]);
|
||||
const dist = new Map();
|
||||
distinct = (row) => {
|
||||
let uniq = '';
|
||||
for (const field of distFields) {
|
||||
const value = row[field];
|
||||
uniq += `${(value === undefined ? '___' : '')}${field}:${value}`;
|
||||
}
|
||||
|
||||
if (dist.has(uniq))
|
||||
return false;
|
||||
dist.set(uniq, true);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
if (!query.where && !query.distinct && query.count) {//some optimization
|
||||
found = [{count: this.rowsInterface.getAllIdsSize()}];
|
||||
} else {//full running
|
||||
for (const id of ids) {
|
||||
const row = await this.rowsInterface.getRow(id);
|
||||
|
||||
if (row && distinct(row)) {
|
||||
found.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.count) {
|
||||
found = [{count: found.length}];
|
||||
}
|
||||
}
|
||||
|
||||
let result = [];
|
||||
if (query.map) {
|
||||
const mapFunc = new Function(`return ${query.map}`)();
|
||||
|
||||
for (const row of found) {
|
||||
result.push(mapFunc(row));
|
||||
}
|
||||
} else {
|
||||
result = found;
|
||||
}
|
||||
|
||||
if (query.sort) {
|
||||
const sortFunc = new Function(`return ${query.sort}`)();
|
||||
result.sort(sortFunc);
|
||||
}
|
||||
|
||||
if (query.hasOwnProperty('limit') || query.hasOwnProperty('offset')) {
|
||||
const offset = query.offset || 0;
|
||||
const limit = (query.hasOwnProperty('limit') ? query.limit : result.length);
|
||||
result = result.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
return utils.cloneDeep(result);
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
replace: Boolean,
|
||||
(!) rows: Array,
|
||||
}
|
||||
result = {
|
||||
(!) inserted: Number,
|
||||
(!) replaced: Number,
|
||||
}
|
||||
*/
|
||||
async insert(query = {}) {
|
||||
await this.openingLock.get(false);
|
||||
this.checkErrors();
|
||||
|
||||
await this.lock.get();
|
||||
try {
|
||||
if (!Array.isArray(query.rows)) {
|
||||
throw new Error('query.rows must be an array');
|
||||
}
|
||||
|
||||
const newRows = utils.cloneDeep(query.rows);
|
||||
const replace = query.replace;
|
||||
|
||||
//autoIncrement correction
|
||||
for (const newRow of newRows) {
|
||||
if (typeof(newRow.id) === 'number' && newRow.id >= this.autoIncrement)
|
||||
this.autoIncrement = newRow.id + 1;
|
||||
}
|
||||
|
||||
const oldRows = [];
|
||||
const newRowsStr = [];
|
||||
//checks
|
||||
for (const newRow of newRows) {
|
||||
if (newRow.hasOwnProperty('___meta'))
|
||||
throw new Error(`Use of field with name '___meta' is forbidden`);
|
||||
|
||||
if (newRow.id === undefined) {
|
||||
newRow.id = this.autoIncrement;
|
||||
this.autoIncrement++;
|
||||
}
|
||||
|
||||
const t = typeof(newRow.id);
|
||||
if (t !== 'number' && t !== 'string') {
|
||||
throw new Error(`Row id bad type, 'number' or 'string' expected, got ${t}`);
|
||||
}
|
||||
|
||||
const oldRow = await this.rowsInterface.getRow(newRow.id);
|
||||
|
||||
if (!replace && oldRow) {
|
||||
throw new Error(`Record id:${newRow.id} already exists`);
|
||||
}
|
||||
|
||||
oldRows.push((oldRow ? oldRow : {}));
|
||||
newRowsStr.push(JSON.stringify(newRow));//because of stringify errors
|
||||
}
|
||||
|
||||
const result = {inserted: 0, replaced: 0};
|
||||
this.deltaStep++;
|
||||
try {
|
||||
//reducer
|
||||
this.reducer._update(oldRows, newRows, this.deltaStep);
|
||||
|
||||
//insert
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
const newRow = newRows[i];
|
||||
const newRowStr = newRowsStr[i];
|
||||
const oldRow = oldRows[i];
|
||||
|
||||
this.rowsInterface.setRow(newRow.id, newRow, newRowStr, this.deltaStep);
|
||||
|
||||
if (oldRow.id !== undefined)
|
||||
result.replaced++;
|
||||
else
|
||||
result.inserted++;
|
||||
}
|
||||
|
||||
this.changes.push([this.deltaStep, 1]);
|
||||
} catch(e) {
|
||||
this.changes.push([this.deltaStep, 0]);
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.waitForSaveChanges();
|
||||
return result;
|
||||
} finally {
|
||||
this.saveChanges();//no await
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
(!) mod: '(r) => r.count++',
|
||||
where: `@@index('field1', 10, 20)`,
|
||||
sort: '(a, b) => a.id - b.id',
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
}
|
||||
result = {
|
||||
(!) updated: Number,
|
||||
}
|
||||
*/
|
||||
async update(query = {}) {
|
||||
await this.openingLock.get(false);
|
||||
this.checkErrors();
|
||||
|
||||
await this.lock.get();
|
||||
try {
|
||||
if (typeof(query.mod) !== 'string') {
|
||||
throw new Error('query.mod must be a string');
|
||||
}
|
||||
const modFunc = new Function(`return ${query.mod}`)();
|
||||
|
||||
//where
|
||||
let ids;//iterator
|
||||
if (query.where) {
|
||||
const where = this.prepareWhere(query.where);
|
||||
const whereFunc = new Function(`return ${where}`)();
|
||||
|
||||
ids = await whereFunc(this.reducer);
|
||||
} else {
|
||||
ids = this.rowsInterface.getAllIds();
|
||||
}
|
||||
|
||||
//oldRows
|
||||
let oldRows = [];
|
||||
for (const id of ids) {
|
||||
const oldRow = await this.rowsInterface.getRow(id);
|
||||
|
||||
if (oldRow) {
|
||||
oldRows.push(oldRow);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.sort) {
|
||||
const sortFunc = new Function(`return ${query.sort}`)();
|
||||
oldRows.sort(sortFunc);
|
||||
}
|
||||
let newRows = utils.cloneDeep(oldRows);
|
||||
|
||||
if (query.hasOwnProperty('limit') || query.hasOwnProperty('offset')) {
|
||||
const offset = query.offset || 0;
|
||||
const limit = (query.hasOwnProperty('limit') ? query.limit : newRows.length);
|
||||
newRows = newRows.slice(offset, offset + limit);
|
||||
oldRows = oldRows.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
//mod & checks
|
||||
const context = {};
|
||||
const newRowsStr = [];
|
||||
for (const newRow of newRows) {
|
||||
modFunc(newRow, context);
|
||||
|
||||
const t = typeof(newRow.id);
|
||||
if (t !== 'number' && t !== 'string') {
|
||||
throw new Error(`Row id bad type, 'number' or 'string' expected, got ${t}`);
|
||||
}
|
||||
|
||||
//autoIncrement correction
|
||||
if (t === 'number' && newRow.id >= this.autoIncrement)
|
||||
this.autoIncrement = newRow.id + 1;
|
||||
|
||||
if (newRow.hasOwnProperty('___meta'))
|
||||
throw new Error(`Use of field with name '___meta' is forbidden`);
|
||||
|
||||
newRowsStr.push(JSON.stringify(newRow));//because of stringify errors
|
||||
}
|
||||
|
||||
this.deltaStep++;
|
||||
const result = {updated: 0};
|
||||
try {
|
||||
//reducer
|
||||
this.reducer._update(oldRows, newRows, this.deltaStep);
|
||||
|
||||
//replace
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
const newRow = newRows[i];
|
||||
const newRowStr = newRowsStr[i];
|
||||
|
||||
// oldRow.id === newRow.id always here, so
|
||||
this.rowsInterface.setRow(newRow.id, newRow, newRowStr, this.deltaStep);
|
||||
|
||||
result.updated++;
|
||||
}
|
||||
|
||||
this.changes.push([this.deltaStep, 1]);
|
||||
} catch(e) {
|
||||
this.changes.push([this.deltaStep, 0]);
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.waitForSaveChanges();
|
||||
return result;
|
||||
} finally {
|
||||
this.saveChanges();//no await
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
query = {
|
||||
where: `@@index('field1', 10, 20)`,
|
||||
sort: '(a, b) => a.id - b.id',
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
}
|
||||
result = {
|
||||
(!) deleted: Number,
|
||||
}
|
||||
*/
|
||||
async delete(query = {}) {
|
||||
await this.openingLock.get(false);
|
||||
this.checkErrors();
|
||||
|
||||
await this.lock.get();
|
||||
try {
|
||||
//where
|
||||
let ids;//iterator
|
||||
if (query.where) {
|
||||
const where = this.prepareWhere(query.where);
|
||||
const whereFunc = new Function(`return ${where}`)();
|
||||
|
||||
ids = await whereFunc(this.reducer);
|
||||
} else {
|
||||
ids = this.rowsInterface.getAllIds();
|
||||
}
|
||||
|
||||
//oldRows
|
||||
let oldRows = [];
|
||||
let newRows = [];
|
||||
for (const id of ids) {
|
||||
const oldRow = await this.rowsInterface.getRow(id);
|
||||
|
||||
if (oldRow) {
|
||||
oldRows.push(oldRow);
|
||||
newRows.push({});
|
||||
}
|
||||
}
|
||||
|
||||
if (query.sort) {
|
||||
const sortFunc = new Function(`return ${query.sort}`)();
|
||||
oldRows.sort(sortFunc);
|
||||
}
|
||||
|
||||
if (query.hasOwnProperty('limit') || query.hasOwnProperty('offset')) {
|
||||
const offset = query.offset || 0;
|
||||
const limit = (query.hasOwnProperty('limit') ? query.limit : newRows.length);
|
||||
newRows = newRows.slice(offset, offset + limit);
|
||||
oldRows = oldRows.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
this.deltaStep++;
|
||||
const result = {deleted: 0};
|
||||
try {
|
||||
//reducer
|
||||
this.reducer._update(oldRows, newRows, this.deltaStep);
|
||||
|
||||
//delete
|
||||
for (let i = 0; i < oldRows.length; i++) {
|
||||
const oldRow = oldRows[i];
|
||||
|
||||
this.rowsInterface.deleteRow(oldRow.id, this.deltaStep);
|
||||
|
||||
result.deleted++;
|
||||
}
|
||||
|
||||
this.changes.push([this.deltaStep, 1]);
|
||||
} catch(e) {
|
||||
this.changes.push([this.deltaStep, 0]);
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.waitForSaveChanges();
|
||||
return result;
|
||||
} finally {
|
||||
this.saveChanges();//no await
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
async saveState(state) {
|
||||
await fs.writeFile(`${this.tablePath}/state`, state);
|
||||
}
|
||||
|
||||
async saveChanges() {
|
||||
this.needSaveChanges = true;
|
||||
if (this.savingChanges)
|
||||
return;
|
||||
|
||||
if (this.inMemory) {
|
||||
this.changes = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.checkErrors();
|
||||
} catch(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.savingChanges = true;
|
||||
try {
|
||||
await utils.sleep(0);
|
||||
|
||||
while (this.needSaveChanges) {
|
||||
this.needSaveChanges = false;
|
||||
|
||||
await this.saveState('0');
|
||||
while (this.changes.length) {
|
||||
|
||||
const len = this.changes.length;
|
||||
let i = 0;
|
||||
while (i < len) {
|
||||
const [deltaStep, isOk] = this.changes[i];
|
||||
i++;
|
||||
|
||||
if (isOk) {
|
||||
await this.tableRowsFile.saveDelta(deltaStep);
|
||||
await this.reducer._saveDelta(deltaStep);
|
||||
} else {
|
||||
await this.tableRowsFile.cancelDelta(deltaStep);
|
||||
await this.reducer._cancelDelta(deltaStep);
|
||||
}
|
||||
}
|
||||
|
||||
this.changes = this.changes.slice(i);
|
||||
}
|
||||
await this.saveState('1');
|
||||
|
||||
if (this.forceFileClosing) {
|
||||
await this.tableRowsFile.closeAllFiles();
|
||||
await this.reducer._closeAllFiles();
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e.message);
|
||||
this.fileError = e.message;
|
||||
} finally {
|
||||
this.savingChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Table;
|
||||
@@ -1,22 +0,0 @@
|
||||
class TableFlag {
|
||||
constructor(checkCode) {
|
||||
this.checkCode = checkCode;
|
||||
this.checkFunc = eval(checkCode);
|
||||
|
||||
this.flag = new Set();
|
||||
}
|
||||
|
||||
add(row) {
|
||||
if (this.checkFunc(row)) {
|
||||
this.flag.add(row.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
del(row) {
|
||||
this.flag.delete(row.id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TableFlag;
|
||||
@@ -1,172 +0,0 @@
|
||||
class TableHash {
|
||||
//opts.type = 'string' || 'number' || 'number_as_string'
|
||||
constructor(opts = {}) {
|
||||
const type = opts.type || 'string';
|
||||
this.depth = opts.depth || 11;
|
||||
this.allowUndef = opts.allowUndef || false;
|
||||
this.unique = opts.unique || false;
|
||||
|
||||
this.hash = new Map();
|
||||
|
||||
this.isNumber = (type === 'number' || type === 'number_as_string');
|
||||
this.numberAsString = (type === 'number_as_string');
|
||||
this.valueAsString = !this.isNumber || this.numberAsString;
|
||||
}
|
||||
|
||||
checkType(v) {
|
||||
if (typeof(v) != 'number' && this.isNumber)
|
||||
throw new Error(`Hashed value must be a number, got type:${typeof(v)}, value:${v}`);
|
||||
|
||||
if (typeof(v) != 'string' && !this.isNumber)
|
||||
throw new Error(`Hashed value must be a string, got type:${typeof(v)}, value:${v}`);
|
||||
}
|
||||
|
||||
prepareValue(v) {
|
||||
let result = v;
|
||||
if (this.numberAsString) {
|
||||
result = v.toString().padStart(this.depth, '0');
|
||||
}
|
||||
if (this.valueAsString && result.length > this.depth)
|
||||
result = result.substring(0, this.depth);
|
||||
return result;
|
||||
}
|
||||
|
||||
add(value, id) {
|
||||
if (value === undefined && this.allowUndef)
|
||||
return;
|
||||
|
||||
this.checkType(value);
|
||||
|
||||
value = this.prepareValue(value);
|
||||
if (this.hash.has(value)) {
|
||||
if (this.unique) {
|
||||
const id_ = this.hash.get(value);
|
||||
if (id_ !== id) {
|
||||
throw new Error(`Collision for unique hash detected: value:${value}, id1:${id_}, id2:${id}`);
|
||||
}
|
||||
} else {
|
||||
const ids = this.hash.get(value);
|
||||
ids.add(id);
|
||||
}
|
||||
} else {
|
||||
if (this.unique) {
|
||||
this.hash.set(value, id);
|
||||
} else {
|
||||
const ids = new Set();
|
||||
this.hash.set(value, ids);
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
del(value, id) {
|
||||
if (value === undefined && this.allowUndef)
|
||||
return;
|
||||
|
||||
this.checkType(value);
|
||||
|
||||
value = this.prepareValue(value);
|
||||
if (this.hash.has(value)) {
|
||||
if (this.unique) {
|
||||
const id_ = this.hash.get(value);
|
||||
if (id_ === id)
|
||||
this.hash.delete(value);
|
||||
} else {
|
||||
const ids = this.hash.get(value);
|
||||
|
||||
ids.delete(id);
|
||||
|
||||
if (!ids.size) {
|
||||
this.hash.delete(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
reduce(value) {
|
||||
this.checkType(value);
|
||||
|
||||
value = this.prepareValue(value);
|
||||
let result;
|
||||
if (this.hash.has(value)) {
|
||||
if (this.unique) {
|
||||
result = new Set();
|
||||
result.add(this.hash.get(value));
|
||||
} else {
|
||||
result = this.hash.get(value);
|
||||
}
|
||||
} else {
|
||||
result = new Set();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
min() {
|
||||
let result = new Set();
|
||||
|
||||
let min = null;
|
||||
let id = null;
|
||||
for (const value of this.hash.keys()) {
|
||||
if (value < min || min === null) {
|
||||
min = value;
|
||||
id = this.hash.get(min);
|
||||
}
|
||||
}
|
||||
|
||||
if (id !== null) {
|
||||
if (this.unique)
|
||||
result.add(id);
|
||||
else
|
||||
result = id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
max() {
|
||||
let result = new Set();
|
||||
|
||||
let max = null;
|
||||
let id = null;
|
||||
for (const value of this.hash.keys()) {
|
||||
if (value > max || max === null) {
|
||||
max = value;
|
||||
id = this.hash.get(max);
|
||||
}
|
||||
}
|
||||
|
||||
if (id !== null) {
|
||||
if (this.unique)
|
||||
result.add(id);
|
||||
else
|
||||
result = id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
iter(checkFunc) {
|
||||
const result = new Set();
|
||||
for (const [value, ids] of this.hash.entries()) {
|
||||
const checkResult = checkFunc(value);
|
||||
if (checkResult === undefined)
|
||||
break;
|
||||
if (checkResult) {
|
||||
if (this.unique) {
|
||||
result.add(ids);
|
||||
} else {
|
||||
for (const id of ids)
|
||||
result.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TableHash;
|
||||
@@ -1,311 +0,0 @@
|
||||
const utils = require('./utils');
|
||||
|
||||
class TableIndex {
|
||||
//opts.type = 'string' || 'number' || 'number_as_string'
|
||||
constructor(opts = {}) {
|
||||
const type = opts.type || 'string';
|
||||
this.depth = opts.depth || 11;
|
||||
this.allowUndef = opts.allowUndef || false;
|
||||
this.unique = opts.unique || false;
|
||||
|
||||
this.hash = new Map();
|
||||
this.sorted = [[]];
|
||||
this.delCount = 0;
|
||||
|
||||
this.isNumber = (type === 'number' || type === 'number_as_string');
|
||||
this.numberAsString = (type === 'number_as_string');
|
||||
this.valueAsString = !this.isNumber || this.numberAsString;
|
||||
|
||||
this.cmp = (a, b) => a.localeCompare(b);
|
||||
if (type === 'number') {
|
||||
this.cmp = (a, b) => a - b;
|
||||
} else if (type === 'number_as_string') {
|
||||
this.cmp = (a, b) => (a < b ? -1 : (a > b ? 1 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
checkType(v) {
|
||||
if (typeof(v) != 'number' && this.isNumber)
|
||||
throw new Error(`Indexed value must be a number, got type:${typeof(v)}, value:${v}`);
|
||||
|
||||
if (typeof(v) != 'string' && !this.isNumber)
|
||||
throw new Error(`Indexed value must be a string, got type:${typeof(v)}, value:${v}`);
|
||||
}
|
||||
|
||||
prepareValue(v) {
|
||||
let result = v;
|
||||
if (this.numberAsString) {
|
||||
result = v.toString().padStart(this.depth, '0');
|
||||
}
|
||||
if (this.valueAsString && result.length > this.depth)
|
||||
result = result.substring(0, this.depth);
|
||||
return result;
|
||||
}
|
||||
|
||||
add(value, id) {
|
||||
if (value === undefined && this.allowUndef)
|
||||
return;
|
||||
|
||||
this.checkType(value);
|
||||
|
||||
value = this.prepareValue(value);
|
||||
if (this.hash.has(value)) {
|
||||
if (this.unique) {
|
||||
const id_ = this.hash.get(value);
|
||||
if (id_ !== id) {
|
||||
throw new Error(`Collision for unique index detected: value:${value}, id1:${id_}, id2:${id}`);
|
||||
}
|
||||
} else {
|
||||
const ids = this.hash.get(value);
|
||||
ids.add(id);
|
||||
}
|
||||
} else {
|
||||
if (this.unique) {
|
||||
this.hash.set(value, id);
|
||||
} else {
|
||||
const ids = new Set();
|
||||
this.hash.set(value, ids);
|
||||
ids.add(id);
|
||||
}
|
||||
|
||||
let s = this.sorted.length - 1;
|
||||
const d = this.sorted[s];
|
||||
d.push(value);
|
||||
|
||||
let i = d.length - 1;
|
||||
//вставка
|
||||
while (i > 0 && this.cmp(d[i], d[i - 1]) < 0) {
|
||||
const v = d[i];
|
||||
d[i] = d[i - 1];
|
||||
d[i - 1] = v;
|
||||
i--;
|
||||
}
|
||||
|
||||
if (d.length > 10) {
|
||||
//слияние
|
||||
while (s > 0 && this.sorted[s].length >= this.sorted[s - 1].length) {
|
||||
const a = this.sorted.pop();
|
||||
const b = this.sorted.pop();
|
||||
const c = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < a.length || j < b.length) {
|
||||
if (i < a.length && (j === b.length || this.cmp(a[i], b[j]) <= 0)) {
|
||||
c.push(a[i]);
|
||||
i++;
|
||||
}
|
||||
if (j < b.length && (i === a.length || this.cmp(b[j], a[i]) <= 0)) {
|
||||
c.push(b[j]);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
this.sorted.push(c);
|
||||
s--;
|
||||
}
|
||||
|
||||
this.sorted.push([]);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
del(value, id, forceClean = false) {
|
||||
if (value === undefined && this.allowUndef)
|
||||
return;
|
||||
|
||||
this.checkType(value);
|
||||
|
||||
value = this.prepareValue(value);
|
||||
if (this.hash.has(value)) {
|
||||
if (this.unique) {
|
||||
const id_ = this.hash.get(value);
|
||||
if (id_ === id) {
|
||||
this.hash.delete(value);
|
||||
this.delCount++;
|
||||
}
|
||||
} else {
|
||||
const ids = this.hash.get(value);
|
||||
|
||||
ids.delete(id);
|
||||
|
||||
if (!ids.size) {
|
||||
this.hash.delete(value);
|
||||
this.delCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.delCount > (this.sorted[0].length >> 2) || forceClean) {
|
||||
for (let s = 0; s < this.sorted.length; s++) {
|
||||
const a = this.sorted[s];
|
||||
const b = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (this.hash.has(a[i]))
|
||||
b.push(a[i]);
|
||||
}
|
||||
this.sorted[s] = b;
|
||||
}
|
||||
|
||||
this.sorted = this.sorted.filter(a => a.length);
|
||||
if (!this.sorted.length) {
|
||||
this.sorted = [[]]
|
||||
} else {
|
||||
this.sorted.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
this.delCount = 0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
reduce(from, to) {
|
||||
const useFrom = (from !== undefined);
|
||||
const useTo = (to !== undefined);
|
||||
|
||||
if (useFrom) {
|
||||
this.checkType(from);
|
||||
from = this.prepareValue(from);
|
||||
}
|
||||
if (useTo) {
|
||||
this.checkType(to);
|
||||
to = this.prepareValue(to);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (let s = 0; s < this.sorted.length; s++) {
|
||||
const a = this.sorted[s];
|
||||
if (!a.length) // на всякий случай
|
||||
continue;
|
||||
|
||||
let leftIndex = 0;
|
||||
if (useFrom) {
|
||||
//дихотомия
|
||||
let left = 0;
|
||||
let right = a.length - 1;
|
||||
while (left < right) {
|
||||
let mid = left + ((right - left) >> 1);
|
||||
if (this.cmp(from, a[mid]) <= 0)
|
||||
right = mid;
|
||||
else
|
||||
left = mid + 1;
|
||||
}
|
||||
|
||||
leftIndex = right;
|
||||
if (this.cmp(from, a[right]) > 0)
|
||||
leftIndex++;
|
||||
}
|
||||
|
||||
let rightIndex = a.length;
|
||||
if (useTo) {
|
||||
//дихотомия
|
||||
let left = 0;
|
||||
let right = a.length - 1;
|
||||
while (left < right) {
|
||||
let mid = right - ((right - left) >> 1);
|
||||
if (this.cmp(to, a[mid]) >= 0)
|
||||
left = mid;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
|
||||
rightIndex = left;
|
||||
if (this.cmp(to, a[left]) >= 0)
|
||||
rightIndex++;
|
||||
}
|
||||
//console.log(a, leftIndex, rightIndex);
|
||||
if (this.unique) {
|
||||
const ids = new Set();
|
||||
for (let i = leftIndex; i < rightIndex; i++) {
|
||||
const value = a[i];
|
||||
if (this.hash.has(value)) {
|
||||
ids.add(this.hash.get(value));
|
||||
}
|
||||
}
|
||||
result.push(ids);
|
||||
} else {
|
||||
for (let i = leftIndex; i < rightIndex; i++) {
|
||||
const value = a[i];
|
||||
if (this.hash.has(value)) {
|
||||
result.push(this.hash.get(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return utils.unionSet(result);
|
||||
}
|
||||
|
||||
min() {
|
||||
let result = new Set();
|
||||
|
||||
let min = null;
|
||||
let id = null;
|
||||
for (let s = 0; s < this.sorted.length; s++) {
|
||||
const a = this.sorted[s];
|
||||
if (!a.length) // на всякий случай
|
||||
continue;
|
||||
if (a[0] < min || min === null) {
|
||||
min = a[0];
|
||||
id = this.hash.get(min);
|
||||
}
|
||||
}
|
||||
|
||||
if (id !== null) {
|
||||
if (this.unique)
|
||||
result.add(id);
|
||||
else
|
||||
result = id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
max() {
|
||||
let result = new Set();
|
||||
|
||||
let max = null;
|
||||
let id = null;
|
||||
for (let s = 0; s < this.sorted.length; s++) {
|
||||
const a = this.sorted[s];
|
||||
if (!a.length) // на всякий случай
|
||||
continue;
|
||||
|
||||
const last = a.length - 1;
|
||||
if (a[last] > max || max === null) {
|
||||
max = a[last];
|
||||
id = this.hash.get(max);
|
||||
}
|
||||
}
|
||||
|
||||
if (id !== null) {
|
||||
if (this.unique)
|
||||
result.add(id);
|
||||
else
|
||||
result = id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
iter(checkFunc) {
|
||||
const result = new Set();
|
||||
for (const [value, ids] of this.hash.entries()) {
|
||||
const checkResult = checkFunc(value);
|
||||
if (checkResult === undefined)
|
||||
break;
|
||||
if (checkResult) {
|
||||
if (this.unique) {
|
||||
result.add(ids);
|
||||
} else {
|
||||
for (const id of ids)
|
||||
result.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TableIndex;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,646 +0,0 @@
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const utils = require('./utils');
|
||||
|
||||
const maxBlockSize = 1024*1024;//bytes
|
||||
|
||||
const minFileDumpSize = 100*1024;//bytes
|
||||
const maxFileDumpSize = 50*1024*1024;//bytes
|
||||
const defragAfter = 10;
|
||||
const defragBlockCountAtOnce = 10;//better >= defragAfter
|
||||
|
||||
class TableRowsFile {
|
||||
constructor(tablePath, cacheSize, compressed) {
|
||||
this.tablePath = tablePath;
|
||||
this.loadedBlocksCount = cacheSize || 5;
|
||||
this.loadedBlocksCount = (this.loadedBlocksCount <= 0 ? 0 : this.loadedBlocksCount);
|
||||
this.compressed = compressed || 0;
|
||||
|
||||
this.blockIndex = new Map();
|
||||
this.currentBlockIndex = 0;
|
||||
this.lastSavedBlockIndex = 0;
|
||||
this.blockList = new Map();
|
||||
this.blocksNotFinalized = new Map();//indexes of blocks
|
||||
this.loadedBlocks = [];
|
||||
this.deltas = new Map();
|
||||
|
||||
this.defragCounter = 0;
|
||||
this.destroyed = false;
|
||||
|
||||
this.blockindex0Size = 0;
|
||||
this.blocklist0Size = 0;
|
||||
|
||||
this.fd = {
|
||||
blockIndex: null,
|
||||
blockList: null,
|
||||
blockRows: null,
|
||||
blockRowsIndex: null,//not a file descriptor
|
||||
};
|
||||
}
|
||||
|
||||
//--- rows interface
|
||||
async getRow(id) {
|
||||
const block = this.blockList.get(this.blockIndex.get(id));
|
||||
|
||||
if (block) {
|
||||
if (!block.rows) {
|
||||
await this.loadBlock(block);
|
||||
}
|
||||
|
||||
this.unloadBlocksIfNeeded();//no await
|
||||
return block.rows.get(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setRow(id, row, rowStr, deltaStep) {
|
||||
const delta = this.getDelta(deltaStep);
|
||||
|
||||
if (this.blockIndex.has(id)) {
|
||||
this.deleteRow(id, deltaStep, delta);
|
||||
}
|
||||
|
||||
const index = this.addToCurrentBlock(id, row, rowStr, deltaStep, delta);
|
||||
this.blockIndex.set(id, index);
|
||||
delta.blockIndex.push([id, index]);
|
||||
}
|
||||
|
||||
deleteRow(id, deltaStep, delta) {
|
||||
if (this.blockIndex.has(id)) {
|
||||
if (!delta)
|
||||
delta = this.getDelta(deltaStep);
|
||||
|
||||
const block = this.blockList.get(this.blockIndex.get(id));
|
||||
if (block) {
|
||||
block.delCount++;
|
||||
delta.blockList.push([block.index, 1]);
|
||||
}
|
||||
|
||||
this.blockIndex.delete(id);
|
||||
delta.blockIndex.push([id, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
getAllIds() {
|
||||
return this.blockIndex.keys();
|
||||
}
|
||||
|
||||
getAllIdsSize() {
|
||||
return this.blockIndex.size;
|
||||
}
|
||||
//--- rows interface end
|
||||
|
||||
getDelta(deltaStep) {
|
||||
if (this.deltas.has(deltaStep)) {
|
||||
return this.deltas.get(deltaStep);
|
||||
} else {
|
||||
const delta = {
|
||||
blockIndex: [],
|
||||
blockList: [],
|
||||
blockRows: [],
|
||||
};
|
||||
this.deltas.set(deltaStep, delta);
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
createNewBlock() {
|
||||
this.currentBlockIndex++;
|
||||
const block = {
|
||||
index: this.currentBlockIndex,
|
||||
delCount: 0,
|
||||
addCount: 0,
|
||||
size: 0,
|
||||
rows: new Map(),
|
||||
rowsLength: 0,
|
||||
final: false,
|
||||
};
|
||||
this.blockList.set(this.currentBlockIndex, block);
|
||||
this.loadedBlocks.push(this.currentBlockIndex);
|
||||
this.blocksNotFinalized.set(this.currentBlockIndex, 1);
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
addToCurrentBlock(id, row, rowStr, deltaStep, delta) {
|
||||
if (!delta)
|
||||
delta = this.getDelta(deltaStep);
|
||||
|
||||
let block = this.blockList.get(this.currentBlockIndex);
|
||||
if (!block)
|
||||
block = this.createNewBlock();
|
||||
|
||||
if (block.size > maxBlockSize)
|
||||
block = this.createNewBlock();
|
||||
|
||||
if (!block.rows) {
|
||||
throw new Error('TableRowsFile: something has gone wrong');
|
||||
}
|
||||
|
||||
block.rows.set(id, row);
|
||||
|
||||
block.addCount++;
|
||||
block.size += rowStr.length;
|
||||
block.rowsLength = block.rows.size;
|
||||
|
||||
delta.blockList.push([block.index, 1]);
|
||||
delta.blockRows.push([block.index, id, row]);
|
||||
|
||||
return block.index;
|
||||
}
|
||||
|
||||
async unloadBlocksIfNeeded() {
|
||||
this.needUnload = true;
|
||||
if (this.unloadingBlocks)
|
||||
return;
|
||||
|
||||
this.unloadingBlocks = true;
|
||||
try {
|
||||
while (this.needUnload) {
|
||||
this.needUnload = false;
|
||||
if (this.destroyed)
|
||||
return;
|
||||
|
||||
await utils.sleep(10);
|
||||
|
||||
//check loaded
|
||||
let missed = new Map();
|
||||
while (this.loadedBlocks.length >= this.loadedBlocksCount) {
|
||||
const index = this.loadedBlocks.shift();
|
||||
if (index >= this.lastSavedBlockIndex) {
|
||||
missed.set(index, 1);
|
||||
continue;
|
||||
}
|
||||
const block = this.blockList.get(index);
|
||||
|
||||
if (block) {
|
||||
block.rows = null;
|
||||
//console.log(`unloaded block ${block.index}`);
|
||||
}
|
||||
|
||||
if (this.destroyed)
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedBlocks = this.loadedBlocks.concat(Array.from(missed.keys()));
|
||||
}
|
||||
} finally {
|
||||
this.unloadingBlocks = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadFile(filePath) {
|
||||
let buf = await fs.readFile(filePath);
|
||||
if (!buf.length)
|
||||
throw new Error(`TableRowsFile: file ${filePath} is empty`);
|
||||
|
||||
const flag = buf[0];
|
||||
if (flag === 50) {//flag '2' ~ finalized && compressed
|
||||
const packed = Buffer.from(buf.buffer, buf.byteOffset + 1, buf.length - 1);
|
||||
const data = await utils.inflate(packed);
|
||||
buf = data.toString();
|
||||
} else if (flag === 49) {//flag '1' ~ finalized
|
||||
buf[0] = 32;//' '
|
||||
buf = buf.toString();
|
||||
} else {//flag '0' ~ not finalized
|
||||
buf[0] = 32;//' '
|
||||
const last = buf.length - 1;
|
||||
if (buf[last] === 44) {//','
|
||||
buf[last] = 93;//']'
|
||||
buf = buf.toString();
|
||||
} else {//corrupted or empty
|
||||
buf = buf.toString();
|
||||
if (this.loadCorrupted) {
|
||||
const lastComma = buf.lastIndexOf(',');
|
||||
if (lastComma >= 0)
|
||||
buf = buf.substring(0, lastComma);
|
||||
}
|
||||
buf += ']';
|
||||
}
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(buf);
|
||||
} catch(e) {
|
||||
throw new Error(`load ${filePath} failed: ${e.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async writeFinal(fileName, data) {
|
||||
if (!this.compressed) {
|
||||
await fs.writeFile(fileName, '1' + data);
|
||||
} else {
|
||||
let buf = Buffer.from(data);
|
||||
buf = await utils.deflate(buf, this.compressed);
|
||||
const fd = await fs.open(fileName, 'w');
|
||||
await fd.write('2');
|
||||
await fd.write(buf);
|
||||
await fd.close();
|
||||
}
|
||||
}
|
||||
|
||||
async loadBlock(block) {
|
||||
//console.log(`start load block ${block.index}`);
|
||||
if (!block.rows) {
|
||||
const arr = await this.loadFile(this.blockRowsFilePath(block.index));
|
||||
|
||||
block.rows = new Map(arr);
|
||||
|
||||
this.loadedBlocks.push(block.index);
|
||||
//console.log(`loaded block ${block.index}`);
|
||||
}
|
||||
}
|
||||
|
||||
async closeFd(name) {
|
||||
if (this.fd[name]) {
|
||||
await this.fd[name].close();
|
||||
this.fd[name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
async openFd(name, fileName = '') {
|
||||
if (this.fd[name])
|
||||
return;
|
||||
|
||||
if (!fileName) {
|
||||
throw new Error('TableRowsFile: fileName is empty');
|
||||
}
|
||||
|
||||
const exists = await utils.pathExists(fileName);
|
||||
|
||||
const fd = await fs.open(fileName, 'a');
|
||||
if (!exists) {
|
||||
await fd.write('0[');
|
||||
}
|
||||
|
||||
this.fd[name] = fd;
|
||||
}
|
||||
|
||||
blockRowsFilePath(index) {
|
||||
if (index < 1000000)
|
||||
return `${this.tablePath}/${index.toString().padStart(6, '0')}.jem`;
|
||||
else
|
||||
return `${this.tablePath}/${index.toString().padStart(12, '0')}.jem`;
|
||||
}
|
||||
|
||||
async finalizeBlocks() {
|
||||
//console.log(this.blocksNotFinalized.size);
|
||||
|
||||
for (const index of this.blocksNotFinalized.keys()) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
|
||||
if (index >= this.lastSavedBlockIndex)
|
||||
continue;
|
||||
|
||||
const block = this.blockList.get(index);
|
||||
|
||||
if (block) {
|
||||
if (block.final)
|
||||
throw new Error('finalizeBlocks: something wrong');
|
||||
|
||||
const blockPath = this.blockRowsFilePath(block.index);
|
||||
//console.log(`start finalize block ${block.index}`);
|
||||
const arr = await this.loadFile(blockPath);
|
||||
const rows = new Map(arr);
|
||||
|
||||
const finBlockPath = `${blockPath}.tmp`;
|
||||
const rowsStr = JSON.stringify(Array.from(rows));
|
||||
await this.writeFinal(finBlockPath, rowsStr);
|
||||
|
||||
await fs.rename(finBlockPath, blockPath);
|
||||
|
||||
block.size = Buffer.byteLength(rowsStr, 'utf8') + 1;
|
||||
block.rowsLength = rows.size;//insurance
|
||||
block.final = true;
|
||||
await this.fd.blockList.write(JSON.stringify(block) + ',');
|
||||
//console.log(`finalized block ${block.index}`);
|
||||
}
|
||||
|
||||
this.blocksNotFinalized.delete(index);
|
||||
}
|
||||
}
|
||||
|
||||
async dumpMaps() {
|
||||
//dumping blockIndex
|
||||
const blockindex1Size = (await this.fd.blockIndex.stat()).size;
|
||||
if ((blockindex1Size > minFileDumpSize && blockindex1Size > this.blockindex0Size) || blockindex1Size > maxFileDumpSize) {
|
||||
const blockindex0Path = `${this.tablePath}/blockindex.0`;
|
||||
const blockindex2Path = `${this.tablePath}/blockindex.2`;
|
||||
await this.writeFinal(blockindex2Path, JSON.stringify(Array.from(this.blockIndex)));
|
||||
|
||||
await fs.rename(blockindex2Path, blockindex0Path);
|
||||
await this.closeFd('blockIndex');
|
||||
await fs.unlink(`${this.tablePath}/blockindex.1`);
|
||||
this.blockindex0Size = (await fs.stat(blockindex0Path)).size;
|
||||
}
|
||||
|
||||
//dumping blockList
|
||||
const blocklist1Size = (await this.fd.blockList.stat()).size;
|
||||
if ((blocklist1Size > minFileDumpSize && blocklist1Size > this.blocklist0Size) || blocklist1Size > maxFileDumpSize) {
|
||||
const blocklist0Path = `${this.tablePath}/blocklist.0`;
|
||||
const blocklist2Path = `${this.tablePath}/blocklist.2`;
|
||||
await this.writeFinal(blocklist2Path, JSON.stringify(Array.from(this.blockList.values())));
|
||||
|
||||
await fs.rename(blocklist2Path, blocklist0Path);
|
||||
await this.closeFd('blockList');
|
||||
await fs.unlink(`${this.tablePath}/blocklist.1`);
|
||||
this.blocklist0Size = (await fs.stat(blocklist0Path)).size;
|
||||
}
|
||||
}
|
||||
|
||||
async saveDelta(deltaStep) {
|
||||
const delta = this.getDelta(deltaStep);
|
||||
|
||||
//lastSavedBlockIndex
|
||||
const len = delta.blockRows.length;
|
||||
if (len) {
|
||||
this.lastSavedBlockIndex = delta.blockRows[len - 1][0];
|
||||
}
|
||||
|
||||
//check all blocks fragmentation
|
||||
if (!this.defragCandidates)
|
||||
this.defragCandidates = [];
|
||||
|
||||
if (!this.defragCandidates.length) {
|
||||
if (this.defragCounter >= defragAfter) {
|
||||
for (const block of this.blockList.values()) {
|
||||
if (!block.final)
|
||||
continue;
|
||||
|
||||
if (block.addCount - block.delCount < block.rowsLength/2 || block.size < maxBlockSize/2) {
|
||||
this.defragCandidates.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
this.defragCounter = 0;
|
||||
} else {
|
||||
this.defragCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
let defragmented = 0;
|
||||
while (this.defragCandidates.length) {
|
||||
if (defragmented >= defragBlockCountAtOnce || this.destroyed)
|
||||
break;
|
||||
|
||||
const block = this.defragCandidates.shift();
|
||||
|
||||
if (!block.rows) {
|
||||
await this.loadBlock(block);
|
||||
}
|
||||
|
||||
//move all active rows from fragmented block to current
|
||||
for (const [id, row] of block.rows.entries()) {
|
||||
if (this.blockIndex.get(id) === block.index) {
|
||||
const newIndex = this.addToCurrentBlock(id, row, JSON.stringify(row), deltaStep, delta);
|
||||
this.blockIndex.set(id, newIndex);
|
||||
delta.blockIndex.push([id, newIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
this.blockList.delete(block.index);
|
||||
delta.blockList.push([block.index, 0]);
|
||||
|
||||
if (!delta.delFiles)
|
||||
delta.delFiles = [];
|
||||
delta.delFiles.push(this.blockRowsFilePath(block.index));
|
||||
|
||||
defragmented++;
|
||||
//console.log(`defragmented block ${block.index}, size: ${block.size}, addCount: ${block.addCount}, delCount: ${block.delCount}, rowsLength: ${block.rowsLength}`);
|
||||
}
|
||||
|
||||
//blockIndex delta save
|
||||
if (!this.fd.blockIndex)
|
||||
await this.openFd('blockIndex', `${this.tablePath}/blockindex.1`);
|
||||
|
||||
let buf = [];
|
||||
for (const deltaRec of delta.blockIndex) {
|
||||
buf.push(JSON.stringify(deltaRec));
|
||||
}
|
||||
if (buf.length)
|
||||
await this.fd.blockIndex.write(buf.join(',') + ',');
|
||||
|
||||
//blockList delta save
|
||||
if (!this.fd.blockList)
|
||||
await this.openFd('blockList', `${this.tablePath}/blocklist.1`);
|
||||
|
||||
let lastSaved = 0;
|
||||
buf = [];
|
||||
for (const deltaRec of delta.blockList) {
|
||||
const index = deltaRec[0];
|
||||
const exists = deltaRec[1];
|
||||
|
||||
if (exists) {
|
||||
if (lastSaved !== index) {//optimization
|
||||
const block = this.blockList.get(index);
|
||||
if (block)//might be defragmented already
|
||||
buf.push(JSON.stringify(block));
|
||||
lastSaved = index;
|
||||
}
|
||||
} else {
|
||||
buf.push(JSON.stringify({index, deleted: 1}));
|
||||
}
|
||||
}
|
||||
if (buf.length)
|
||||
await this.fd.blockList.write(buf.join(',') + ',');
|
||||
|
||||
//blockRows delta save
|
||||
buf = [];
|
||||
for (const deltaRec of delta.blockRows) {
|
||||
const [index, id, row] = deltaRec;
|
||||
|
||||
if (this.fd.blockRowsIndex !== index) {
|
||||
if (buf.length)
|
||||
await this.fd.blockRows.write(buf.join(',') + ',');
|
||||
buf = [];
|
||||
await this.closeFd('blockRows');
|
||||
this.fd.blockRowsIndex = null;
|
||||
}
|
||||
|
||||
if (!this.fd.blockRows) {
|
||||
const blockPath = this.blockRowsFilePath(index);
|
||||
|
||||
await this.openFd('blockRows', blockPath);
|
||||
this.fd.blockRowsIndex = index;
|
||||
}
|
||||
|
||||
buf.push(JSON.stringify([id, row]));
|
||||
}
|
||||
if (buf.length)
|
||||
await this.fd.blockRows.write(buf.join(',') + ',');
|
||||
|
||||
//blocks finalization
|
||||
await this.finalizeBlocks();
|
||||
this.unloadBlocksIfNeeded();//no await
|
||||
|
||||
//dumps if needed
|
||||
await this.dumpMaps();
|
||||
|
||||
//delete files if needed
|
||||
if (delta.delFiles) {
|
||||
for (const fileName of delta.delFiles) {
|
||||
//console.log(`delete ${fileName}`);
|
||||
if (await utils.pathExists(fileName))
|
||||
await fs.unlink(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
this.deltas.delete(deltaStep);
|
||||
}
|
||||
|
||||
async cancelDelta(deltaStep) {
|
||||
this.deltas.delete(deltaStep);
|
||||
}
|
||||
|
||||
async load() {
|
||||
let autoIncrement = 0;
|
||||
|
||||
const loadBlockIndex = (fileNum, data) => {
|
||||
if (fileNum === 0) {//dumped data
|
||||
this.blockIndex = new Map(data);//much faster
|
||||
for (const id of this.blockIndex.keys()) {
|
||||
if (typeof(id) === 'number' && id >= autoIncrement)
|
||||
autoIncrement = id + 1;
|
||||
}
|
||||
} else {
|
||||
for (const rec of data) {
|
||||
const [id, index] = rec;
|
||||
if (index > 0) {
|
||||
this.blockIndex.set(id, index);
|
||||
if (typeof(id) === 'number' && id >= autoIncrement)
|
||||
autoIncrement = id + 1;
|
||||
} else
|
||||
this.blockIndex.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadBlockList = (data) => {
|
||||
for (const rec of data) {
|
||||
const block = rec;
|
||||
if (block.deleted) {
|
||||
this.blockList.delete(block.index);
|
||||
} else {
|
||||
block.rows = null;
|
||||
this.blockList.set(block.index, block);
|
||||
if (block.index > this.currentBlockIndex)
|
||||
this.currentBlockIndex = block.index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.blockIndex.clear();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const dataPath = `${this.tablePath}/blockindex.${i}`;
|
||||
|
||||
if (await utils.pathExists(dataPath)) {
|
||||
const data = await this.loadFile(dataPath);
|
||||
loadBlockIndex(i, data);
|
||||
}
|
||||
}
|
||||
const blockindex0Path = `${this.tablePath}/blockindex.0`;
|
||||
if (await utils.pathExists(blockindex0Path))
|
||||
this.blockindex0Size = (await fs.stat(blockindex0Path)).size;
|
||||
|
||||
this.currentBlockIndex = 0;
|
||||
this.blockList.clear();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const dataPath = `${this.tablePath}/blocklist.${i}`;
|
||||
|
||||
if (await utils.pathExists(dataPath)) {
|
||||
const data = await this.loadFile(dataPath);
|
||||
loadBlockList(data);
|
||||
}
|
||||
}
|
||||
const blocklist0Path = `${this.tablePath}/blocklist.0`;
|
||||
if (await utils.pathExists(blocklist0Path))
|
||||
this.blocklist0Size = (await fs.stat(blocklist0Path)).size;
|
||||
|
||||
this.lastSavedBlockIndex = this.currentBlockIndex;
|
||||
const currentBlock = this.blockList.get(this.currentBlockIndex);
|
||||
if (currentBlock)
|
||||
await this.loadBlock(currentBlock);
|
||||
|
||||
this.blocksNotFinalized = new Map();
|
||||
for (const block of this.blockList.values()) {
|
||||
if (!block.final)
|
||||
this.blocksNotFinalized.set(block.index, 1);
|
||||
}
|
||||
|
||||
return autoIncrement;
|
||||
}
|
||||
|
||||
async loadCorrupted() {
|
||||
this.loadCorrupted = true;
|
||||
|
||||
const loadBlockIndex = (fileNum, data) => {
|
||||
if (fileNum === 0) {//dumped data
|
||||
this.blockIndex = new Map(data);//much faster
|
||||
} else {
|
||||
for (const rec of data) {
|
||||
const [id, index] = rec;
|
||||
if (index > 0)
|
||||
this.blockIndex.set(id, index);
|
||||
else
|
||||
this.blockIndex.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.blockIndex.clear();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const dataPath = `${this.tablePath}/blockindex.${i}`;
|
||||
|
||||
if (await utils.pathExists(dataPath)) {
|
||||
try {
|
||||
const data = await this.loadFile(dataPath);
|
||||
loadBlockIndex(i, data);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const files = await fs.readdir(this.tablePath, { withFileTypes: true });
|
||||
|
||||
this.blockList.clear();
|
||||
for (const file of files) {
|
||||
if (file.isFile() && path.extname(file.name) == '.jem') {
|
||||
const numStr = path.basename(file.name, '.jem');
|
||||
const index = parseInt(numStr, 10);
|
||||
if (!isNaN(index)) {
|
||||
const block = {
|
||||
index,
|
||||
delCount: 0,
|
||||
addCount: 0,
|
||||
size: 0,
|
||||
rows: null,
|
||||
rowsLength: 0,
|
||||
final: false,
|
||||
};
|
||||
this.blockList.set(block.index, block);
|
||||
//console.log(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async closeAllFiles() {
|
||||
await this.closeFd('blockIndex');
|
||||
await this.closeFd('blockList');
|
||||
await this.closeFd('blockRows');
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
await this.closeAllFiles();
|
||||
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TableRowsFile;
|
||||
@@ -1,34 +0,0 @@
|
||||
class TableRowsMem {
|
||||
constructor() {
|
||||
this.rows = new Map();
|
||||
}
|
||||
|
||||
//--- rows interface
|
||||
async getRow(id) {
|
||||
return this.rows.get(id);
|
||||
}
|
||||
|
||||
setRow(id, row) {
|
||||
this.rows.set(id, row);
|
||||
}
|
||||
|
||||
deleteRow(id) {
|
||||
this.rows.delete(id);
|
||||
}
|
||||
|
||||
getAllIds() {
|
||||
return this.rows.keys();
|
||||
}
|
||||
|
||||
getAllIdsSize() {
|
||||
return this.rows.size;
|
||||
}
|
||||
//--- rows interface end
|
||||
|
||||
async destroy() {
|
||||
//for GC
|
||||
this.rows = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TableRowsMem;
|
||||
@@ -1,7 +0,0 @@
|
||||
const JembaDb = require('./JembaDb');
|
||||
const JembaDbThread = require('./JembaDbThread');
|
||||
|
||||
module.exports = {
|
||||
JembaDb,
|
||||
JembaDbThread,
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
const fsCB = require('fs');
|
||||
const fs = fsCB.promises;
|
||||
const zlib = require('zlib');
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function sleepWithStop(ms, cb = () => {}) {
|
||||
return new Promise(resolve => {
|
||||
const timer = setTimeout(resolve, ms);
|
||||
cb(() => { clearTimeout(timer); resolve(); });
|
||||
});
|
||||
}
|
||||
|
||||
function unionSet(arrSet) {
|
||||
if (!arrSet.length)
|
||||
return new Set();
|
||||
|
||||
let max = 0;
|
||||
let size = arrSet[0].size;
|
||||
for (let i = 1; i < arrSet.length; i++) {
|
||||
if (arrSet[i].size > size) {
|
||||
max = i;
|
||||
size = arrSet[i].size;
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Set(arrSet[max]);
|
||||
for (let i = 0; i < arrSet.length; i++) {
|
||||
if (i === max)
|
||||
continue;
|
||||
|
||||
for (const elem of arrSet[i]) {
|
||||
result.add(elem);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function intersectSet(arrSet) {
|
||||
if (!arrSet.length)
|
||||
return new Set();
|
||||
|
||||
let min = 0;
|
||||
let size = arrSet[0].size;
|
||||
for (let i = 1; i < arrSet.length; i++) {
|
||||
if (arrSet[i].size < size) {
|
||||
min = i;
|
||||
size = arrSet[i].size;
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Set();
|
||||
for (const elem of arrSet[min]) {
|
||||
let inAll = true;
|
||||
for (let i = 0; i < arrSet.length; i++) {
|
||||
if (i === min)
|
||||
continue;
|
||||
if (!arrSet[i].has(elem)) {
|
||||
inAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (inAll)
|
||||
result.add(elem);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function pathExists(path) {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function appendFileToFile(nameFrom, nameTo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const readStream = fsCB.createReadStream(nameFrom);
|
||||
readStream.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
const writeStream = fsCB.createWriteStream(nameTo, {flags: 'a'});
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
writeStream.on('close', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
readStream.pipe(writeStream);
|
||||
});
|
||||
}
|
||||
|
||||
function esc(obj) {
|
||||
return JSON.stringify(obj).replace(/@/g, '\\x40');
|
||||
}
|
||||
|
||||
function paramToArray(param) {
|
||||
return (Array.isArray(param) ? param : [param]);
|
||||
}
|
||||
|
||||
function cloneDeep(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
//async
|
||||
function deflate(buf, compressionLevel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.deflateRaw(buf, {level: compressionLevel}, (err, b) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
resolve(b);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//async
|
||||
function inflate(buf) {
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.inflateRaw(buf, (err, b) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
resolve(b);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
sleep,
|
||||
sleepWithStop,
|
||||
unionSet,
|
||||
intersectSet,
|
||||
pathExists,
|
||||
appendFileToFile,
|
||||
esc,
|
||||
paramToArray,
|
||||
cloneDeep,
|
||||
deflate,
|
||||
inflate,
|
||||
};
|
||||
@@ -8,7 +8,6 @@ const http = require('http');
|
||||
const WebSocket = require ('ws');
|
||||
|
||||
const ayncExit = new (require('./core/AsyncExit'))();
|
||||
ayncExit.init();
|
||||
|
||||
let log = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user