added nearly complete p2p implementation

This commit is contained in:
mrfry 2023-03-20 18:02:45 +01:00
parent 11dacdae64
commit 5c22f575dd
25 changed files with 14320 additions and 12563 deletions

23424
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"ejs": "^3.1.6",
"express": "^4.17.3",
"express-fileupload": "^1.3.1",
"hybrid-crypto-js": "^0.2.4",
"socket.io": "^4.4.1",
"tesseract.js": "^3.0.3",
"ts-node": "^10.7.0",
@ -18,7 +19,7 @@
},
"scripts": {
"start": "node ./dist/server.js",
"dev": "npm run build && NS_THREAD_COUNT=2 NS_DEVEL=1 NS_NOUSER=1 NS_LOGLEVEL=1 node --inspect ./dist/server.js",
"dev": "npm run build && NS_THREAD_COUNT=2 NS_DEVEL=1 NS_NOUSER=1 node --inspect ./dist/server.js",
"build": "tsc && bash -c './scripts/postBuild.sh'",
"export": "tsc && bash -c './scripts/postBuild.sh'",
"test": "NS_NOLOG=1 NS_THREAD_COUNT=1 jest",

1
src/declarations.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'hybrid-crypto-js'

View file

@ -19,7 +19,7 @@
------------------------------------------------------------------------- */
import type { Response, NextFunction, RequestHandler } from 'express'
import type { Request } from '../types/basicTypes'
import type { Request, User } from '../types/basicTypes'
import type { Database } from 'better-sqlite3'
import logger from '../utils/logger'
@ -32,11 +32,13 @@ interface Options {
exceptions: Array<string>
}
export const testUser = {
export const testUser: User = {
id: 19,
avaiblePWRequests: 645,
pwRequestCount: 19,
created: new Date(),
created: new Date().getTime(),
lastLogin: new Date().getTime(),
lastAccess: new Date().getTime(),
pw: '5d146f72-e1b8-4440-a6e3-f22f31810316',
loginCount: 3,
createdBy: 1,

View file

@ -30,7 +30,16 @@ import logger from '../../utils/logger'
import utils from '../../utils/utils'
import auth from '../../middlewares/auth.middleware'
import { SetupData } from '../../server'
import { ModuleType, Request, Submodule } from '../../types/basicTypes'
import {
DataFile,
ModuleSpecificData,
ModuleType,
QuestionDb,
Request,
Submodule,
} from '../../types/basicTypes'
import { loadJSON } from '../../utils/actions'
import { initWorkerPool } from '../../utils/workerPool'
// files
const rootRedirectToFile = 'data/apiRootRedirectTo'
@ -142,7 +151,23 @@ function GetApp(): ModuleType {
// -------------------------------------------------------------------------------------------
const submoduleDatas = setupSubModules(app)
const dbsFile = publicDir + 'questionDbs.json'
// FIXME: is dataFiles only a temp variable? does this cause any problems?
const dataFiles: Array<DataFile> = utils.ReadJSON(dbsFile)
let questionDbs: Array<QuestionDb> = loadJSON(dataFiles, publicDir)
initWorkerPool(() => questionDbs)
const submoduleDatas = setupSubModules(app, {
questionDbs: questionDbs,
getQuestionDbs: () => {
return questionDbs
},
setQuestionDbs: (newQdbs: QuestionDb[]) => {
questionDbs = newQdbs
},
dbsFile: dbsFile,
})
// -------------------------------------------------------------------------------------------
@ -183,7 +208,7 @@ function GetApp(): ModuleType {
function setupSubModules(
parentApp: express.Application,
moduleSpecificData?: any
moduleSpecificData: ModuleSpecificData
): Submodule[] {
const submoduleDir = './submodules/'
const absolutePath = __dirname + '/' + submoduleDir

View file

@ -0,0 +1,955 @@
/* ----------------------------------------------------------------------------
Question Server
GitLab: <https://gitlab.com/MrFry/mrfrys-node-server>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
------------------------------------------------------------------------- */
import { Response } from 'express'
import * as child_process from 'child_process'
import http from 'http'
import logger from '../../../utils/logger'
import {
Request,
SubmoduleData,
Submodule,
PeerInfo,
Subject,
QuestionDb,
User,
} from '../../../types/basicTypes'
import utils from '../../../utils/utils'
import { backupData /*writeData*/ } from '../../../utils/actions'
import { WorkerResult } from '../../../utils/classes'
import dbtools from '../../../utils/dbtools'
import {
createKeyPair,
decrypt,
encrypt,
isKeypairValid,
} from '../../../utils/encryption'
import { doALongTask, msgAllWorker } from '../../../utils/workerPool'
import {
countOfQdb,
countOfQdbs,
createQuestion,
getAvailableQdbIndexes,
removeCacheFromQuestion,
} from '../../../utils/qdbUtils'
// TODO: remove FINALIZE-s and TOTEST-s
// TODO: script to remove from date from certain host (questions / users)
interface MergeResult {
newData: Subject[]
newSubjects: Subject[]
localQdbIndex: number
e: Error
}
interface RemotePeerInfo {
selfInfo: PeerInfo
myPeers: PeerInfo[]
revision?: string
qdbInfo?: {
dbName: string
subjs: {
name: string
count: number
}[]
}[]
}
interface RequestResult<T> {
data?: T
error?: Error
options?: http.RequestOptions
}
interface SyncDataRes {
questionDbs?: QuestionDb[]
remoteInfo?: RemotePeerInfo
encryptedUsers?: string
count: {
qdbs: number
subjects: number
questions: number
}
}
function get<T>(options: http.RequestOptions): Promise<RequestResult<T>> {
return new Promise((resolve) => {
const req = http.get(options, function (res) {
const bodyChunks: Uint8Array[] = []
res.on('data', (chunk) => {
bodyChunks.push(chunk)
}).on('end', () => {
const body = Buffer.concat(bodyChunks).toString()
try {
resolve({ data: JSON.parse(body) })
} catch (e) {
resolve({ error: e, options: options })
}
})
})
req.on('error', function (e) {
resolve({ error: e, options: options })
// reject(e)
})
})
}
function peerToString(peer: PeerInfo) {
return `${peer.host}:${peer.port}`
}
function isPeerSameAs(peer1: PeerInfo, peer2: PeerInfo) {
return peer1.host === peer2.host && peer1.port === peer2.port
}
export function getNewDataSince(subjects: Subject[], date: number): Subject[] {
return subjects
.map((subject) => {
return {
...subject,
Questions: subject.Questions.filter((question) => {
return (question.data.date || 0) > date
}).map((question) => removeCacheFromQuestion(question)),
}
})
.filter((subject) => subject.Questions.length !== 0)
}
export function mergeSubjects(
subjectsToMergeTo: Subject[],
subjectsToMerge: Subject[],
newSubjects: Subject[]
): Subject[] {
return [
...subjectsToMergeTo.map((subj) => {
const newSubjs = subjectsToMerge.filter(
(subjRes) => subjRes.Name === subj.Name
)
if (newSubjs) {
const newQuestions = newSubjs.flatMap((subj) => {
return subj.Questions
})
return {
...subj,
Questions: [...subj.Questions, ...newQuestions],
}
} else {
return subj
}
}),
...newSubjects,
]
}
export function mergeQdbs(
qdbToMergeTo: QuestionDb[],
mergeResults: MergeResult[]
): { mergedQuestionDbs: QuestionDb[]; changedQdbIndexes: number[] } {
const changedQdbIndexes: number[] = []
const mergedQuestionDbs = qdbToMergeTo.map((qdb) => {
const qdbMergeResult = mergeResults.find(
(mergeRes) => mergeRes.localQdbIndex === qdb.index
)
if (qdbMergeResult) {
const mergedQdb = {
...qdb,
data: mergeSubjects(
qdb.data,
qdbMergeResult.newData,
qdbMergeResult.newSubjects
),
}
changedQdbIndexes.push(qdb.index)
return mergedQdb
} else {
// unchanged
return qdb
}
})
return {
mergedQuestionDbs: mergedQuestionDbs,
changedQdbIndexes: changedQdbIndexes,
}
}
async function sendNewDataToWorkers(
mergeResults: MergeResult[],
newQuestionDbs: QuestionDb[]
) {
// FIXME: this might be slow, maybe make a new type of message for workers?
const updatePromises: Promise<WorkerResult[]>[] = []
let newQuestionCount = 0
let newSubjectCount = 0
let newQuestionDbCount = 0
mergeResults.forEach((mergeRes) => {
if (mergeRes.e) {
logger.Log(`There was an error processing the merge!`, 'redbg')
console.error(mergeRes.e)
return
}
mergeRes.newData.forEach((subjectWithNewData) => {
newQuestionCount += subjectWithNewData.Questions.length
updatePromises.push(
msgAllWorker({
type: 'newQuestions',
data: {
subjName: subjectWithNewData.Name,
qdbIndex: mergeRes.localQdbIndex,
newQuestions: subjectWithNewData.Questions,
},
})
)
})
newSubjectCount += mergeRes.newSubjects.length
mergeRes.newSubjects.forEach((newSubject) => {
newQuestionCount += newSubject.Questions.length
updatePromises.push(
msgAllWorker({
type: 'newQuestions',
data: {
subjName: newSubject.Name,
qdbIndex: mergeRes.localQdbIndex,
newQuestions: newSubject.Questions,
},
})
)
})
})
newQuestionDbCount += newQuestionDbs.length
newQuestionDbs.forEach((newQdb) => {
const { subjCount: sc, questionCount: qc } = countOfQdb(newQdb)
newSubjectCount += sc
newQuestionCount += qc
msgAllWorker({
data: newQdb,
type: 'newdb',
})
})
await Promise.all(updatePromises)
return {
newQuestionDbCount: newQuestionDbCount,
newSubjectCount: newSubjectCount,
newQuestionCount: newQuestionCount,
}
}
function writeNewData(
newQuestionDbs: QuestionDb[],
changedQuestionDbs: QuestionDb[]
) {
const qdbsToWrite = [...newQuestionDbs, ...changedQuestionDbs]
qdbsToWrite.forEach((qdb) => {
try {
// FINALIZE: write to file
// writeData(qdb.data, qdb.path)
} catch (e) {
logger.Log(`Error writing ${qdb.name} qdb to file!`, 'redbg')
console.error(e)
}
})
}
function updateLastSync(selfInfo: PeerInfo, newDate: number) {
utils.WriteFile(
JSON.stringify({ ...selfInfo, lastSync: newDate }, null, 2),
selfInfoFile
)
}
function setupQuestionsForMerge(qdb: QuestionDb, peer: PeerInfo) {
return {
...qdb,
data: qdb.data.map((subj) => {
return {
...subj,
Questions: subj.Questions.map((q) => {
const initializedQuestion = q.cache ? q : createQuestion(q)
initializedQuestion.data.source = peerToString(peer)
return initializedQuestion
}),
}
}),
}
}
// files
const peersPath = 'data/p2p/'
const peersFile = peersPath + '/peers.json'
// writes it)
const selfInfoFile = peersPath + '/selfInfo.json'
const thirdPartyPeersFile = peersPath + '/thirdPartyPeers.json'
const keyFile = peersPath + '/key' // key.pub key.priv
function setup(data: SubmoduleData): Submodule {
const {
app,
userDB,
publicdirs,
moduleSpecificData: { questionDbs, setQuestionDbs, getQuestionDbs },
// publicdirs,
} = data
const publicDir = publicdirs[0]
// ---------------------------------------------------------------------------------------
// SETUP
// ---------------------------------------------------------------------------------------
// const publicDir = publicdirs[0]
if (!utils.FileExists(peersFile)) {
logger.Log(
`Warning: peers file was missing, so it was created`,
'yellowbg'
)
utils.CreatePath(peersPath)
utils.WriteFile('[]', peersFile)
}
if (!utils.FileExists(selfInfoFile)) {
logger.Log(
'Self info file for p2p does not exist! P2P functionality will not be loaded',
'redbg'
)
logger.Log(
`File should be at: ${selfInfoFile} with the interface 'PeerInfo'`
)
throw new Error('p2p error')
}
let publicKey: string
let privateKey: string
if (
!utils.FileExists(keyFile + '.priv') ||
!utils.FileExists(keyFile + '.pub')
) {
createKeyPair().then(({ publicKey: pubk, privateKey: privk }) => {
// at first start there won't be a keypair available until this finishes
utils.WriteFile(pubk, keyFile + '.pub')
utils.WriteFile(privk, keyFile + '.priv')
publicKey = pubk
privateKey = privk
})
logger.Log(
'There were no public / private keys for p2p functionality, created new ones',
'yellowbg'
)
} else {
publicKey = utils.ReadFile(keyFile + '.pub')
privateKey = utils.ReadFile(keyFile + '.priv')
// checking only here, because if it got generated in the other branch then it must be good
if (!isKeypairValid(publicKey, privateKey)) {
logger.Log('Loaded keypair is not valid!', 'redbg')
}
}
// TODO: validate peers
let peers: PeerInfo[] = utils.ReadJSON(peersFile)
let selfInfo: PeerInfo = utils.ReadJSON(selfInfoFile)
// self info file is not required to have the publicKey, as it is always added on init
selfInfo.publicKey = publicKey
const filesToWatch = [
{
fname: peersFile,
logMsg: 'Peers file updated',
action: () => {
peers = utils.ReadJSON(peersFile)
},
},
{
fname: selfInfoFile,
logMsg: 'P2P self info file changed',
action: () => {
selfInfo = utils.ReadJSON(selfInfoFile)
selfInfo.publicKey = publicKey
},
},
]
filesToWatch.forEach((ftw) => {
if (utils.FileExists(ftw.fname)) {
utils.WatchFile(ftw.fname, () => {
logger.Log(ftw.logMsg)
ftw.action()
})
ftw.action()
} else {
logger.Log(`File ${ftw.fname} does not exists to watch!`, 'redbg')
}
})
if (peers.length === 0) {
logger.Log(
`Warning: peers file is empty. You probably want to fill it`,
'yellowbg'
)
} else {
logger.Log('Loaded peers: ' + peers.length)
peers.forEach((peer, i) => {
logger.Log(`\t${i}\t"${peer.name}": ${peerToString(peer)}`)
})
}
// ---------------------------------------------------------------------------------------
// FUNCTIONS
// ---------------------------------------------------------------------------------------
function getSelfInfo(includeQdbInfo?: boolean) {
const result: RemotePeerInfo = {
selfInfo: selfInfo,
myPeers: peers,
}
try {
// FIXME: dont log if fails
result.revision = child_process
.execSync('git rev-parse HEAD', { cwd: __dirname })
.toString()
.trim()
} catch (e) {
result.revision = 'Failed to get revision'
}
if (includeQdbInfo) {
result.qdbInfo = getQuestionDbs().map((qdb) => {
return {
dbName: qdb.name,
subjs: qdb.data.map((subj) => {
return {
name: subj.Name,
count: subj.Questions.length,
}
}),
}
})
}
return result
}
function getNewUsersSince(since?: number) {
const users = dbtools.runStatement(
userDB,
`SELECT *
FROM users
WHERE created >= ${since};`
)
return users
}
function updateQdbForLocalUse(qdb: QuestionDb[]) {
const availableIndexes = getAvailableQdbIndexes(
getQuestionDbs(),
qdb.length
)
return qdb.map((qdb, i) => {
return {
...qdb,
index: availableIndexes[i],
path: `${publicDir}questionDbs/${qdb.name}.json'`,
}
})
}
async function getMergeResults(remoteQuestionDbs: QuestionDb[]) {
const mergeJobs: Promise<any>[] = []
const rawNewQuestionDbs: QuestionDb[] = []
remoteQuestionDbs.forEach((remoteQdb) => {
// TODO: warn on qdb differences like shouldSave
const localQdb = getQuestionDbs().find(
(lqdb) => lqdb.name === remoteQdb.name
)
if (!localQdb) {
rawNewQuestionDbs.push(remoteQdb)
} else {
mergeJobs.push(
doALongTask({
type: 'merge',
data: {
localQdbIndex: localQdb.index,
remoteQdb: remoteQdb,
},
})
)
}
})
const mergeResults: MergeResult[] = await Promise.all(mergeJobs)
return {
mergeResults: mergeResults,
rawNewQuestionDbs: rawNewQuestionDbs,
}
}
async function syncData() {
// TOTEST: try with 0 date to merge full dbs
if (peers.length === 0) {
logger.Log(
`There are no peers specified in ${peersFile}, aborting sync`,
'yellowbg'
)
return {
msg: 'No peers specified, aborting',
}
}
// FIXME: this might be blocking the main thread, but not sure how much
logger.Log(
`\tStarting data sync, getting new data from ${logger.C('green')}${
peers.length
}${logger.C()} peers`
)
const lastSync = new Date('2023-01-01').getTime() // FINALIZE date: this is only for testing // selfInfo.lastSync
logger.Log(
`\tLast sync date: ${logger.C('blue')}${new Date(
lastSync
).toLocaleString()}${logger.C()}`
)
const syncStart = new Date().getTime()
const requests = peers.map((peer) => {
const lastSyncWithPeer = new Date('2023-01-01').getTime() // FINALIZE same as above // peer.lastSync || 0
logger.Log(
`\tLast sync with ${logger.C('blue')}${peerToString(
peer
)}${logger.C()}: ${logger.C('blue')}${new Date(
lastSyncWithPeer
).toLocaleString()}${logger.C()}`
)
return new Promise<RequestResult<SyncDataRes & { peer: PeerInfo }>>(
(resolve) => {
get<SyncDataRes>({
host: peer.host,
port: peer.port,
path: `/getnewdatasince?host=${selfInfo.host}${
lastSync ? `&since=${lastSyncWithPeer}` : ''
}`,
}).then((res) => {
resolve({ ...res, data: { ...res.data, peer: peer } })
})
}
)
})
const allResults = await Promise.all(requests)
// -------------------------------------------------------------------------------------------------------
// filtering, transforming, and counting data
// -------------------------------------------------------------------------------------------------------
allResults.forEach((res) => {
if (res.error) {
logger.Log(
`\tError syncing with ${peerToString(res.data.peer)}: ${
res.error.message
}`,
'red'
)
}
})
const resultDataWithoutErrors = allResults
.filter((res) => !res.error)
.map((res) => res.data)
if (resultDataWithoutErrors.length === 0) {
logger.Log(
`No peers returned data without error, aborting sync`,
'redbg'
)
return {
msg: 'No peers returned data without error, aborting sync',
}
}
const resultDataWithoutEmptyDbs = resultDataWithoutErrors.filter(
(res) => {
const qdbCount = res.questionDbs.length
const { subjCount, questionCount } = countOfQdbs(
res.questionDbs
)
logger.Log(
`\t"${logger.C('blue')}${peerToString(
res.peer
)}${logger.C()}" sent "${logger.C(
'green'
)}${qdbCount}${logger.C()}" question DB-s with "${logger.C(
'green'
)}${subjCount.toLocaleString()}${logger.C()}" subjects, and "${logger.C(
'green'
)}${questionCount.toLocaleString()}${logger.C()}" questions`
)
return questionCount > 0
}
)
// TOTEST: even on new subjet and new qdb add! TEST
// add new quesstions to db (QuestionData.source = true)
const resultData = resultDataWithoutEmptyDbs.map((res) => {
return {
...res,
questionDbs: res.questionDbs.map((qdb) => {
return setupQuestionsForMerge(qdb, res.peer)
}),
}
})
// -------------------------------------------------------------------------------------------------------
// third party peers handling
// -------------------------------------------------------------------------------------------------------
const peersHosts = [...peers.map((peer) => peer.host), selfInfo.host]
const thirdPartyPeers = resultData
.map((res) => res.remoteInfo)
.flatMap((x) => {
return x.myPeers.filter(
(recievedPeer) => !peersHosts.includes(recievedPeer.host)
)
})
if (thirdPartyPeers.length > 0) {
logger.Log(
`\tPeers reported ${logger.C('green')}${
thirdPartyPeers.length
}${logger.C()} third party peer(s) not connected to this server.`
)
utils.WriteFile(
JSON.stringify(thirdPartyPeers, null, 2),
thirdPartyPeersFile
)
logger.Log(
`\tSee ${logger.C(
'blue'
)}${thirdPartyPeersFile}${logger.C()} for details`
)
}
// -------------------------------------------------------------------------------------------------------
// new users handlin TOTEST: test
// -------------------------------------------------------------------------------------------------------
let newUsers = 0
const oldUserCount = dbtools.SelectAll(userDB, 'users').length
try {
resultData.forEach((res) => {
if (res.encryptedUsers) {
const decryptedUsers: User[] = JSON.parse(
decrypt(privateKey, res.encryptedUsers)
)
let newUserCount = 0
decryptedUsers.forEach((remoteUser) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...remoteUserWithoutId } = remoteUser
const localUser = dbtools.Select(userDB, 'users', {
pw: remoteUser.pw,
})
if (!localUser) {
// FIXME: users will not have consistend id across servers. This may be
// harmless, will see
dbtools.Insert(userDB, 'users', {
...(remoteUserWithoutId as Omit<User, 'id'>),
sourceHost: peerToString(res.peer),
})
newUserCount += 1
}
})
if (newUserCount > 0) {
newUsers += newUserCount
logger.Log(
`\tAdded ${logger.C(
'green'
)}${newUserCount}${logger.C()} users from "${logger.C(
'blue'
)}${peerToString(res.peer)}${logger.C()}"`
)
}
}
})
} catch (e) {
logger.Log(
'\tError while trying to sync users: ' + e.message,
'redbg'
)
console.error(e)
}
const newUserCount = dbtools.SelectAll(userDB, 'users').length
const hasNewData = resultData.length > 0
if (!hasNewData) {
logger.Log(
`No peers returned any new questions. Sync successfully finished!`,
'green'
)
updateLastSync(selfInfo, syncStart)
return {
msg: 'No peers returned any new questions',
}
}
// -------------------------------------------------------------------------------------------------------
// backup
// -------------------------------------------------------------------------------------------------------
const { subjCount: oldSubjCount, questionCount: oldQuestionCount } =
countOfQdbs(getQuestionDbs())
const oldQuestionDbCount = getQuestionDbs().length
// TOTEST: test if backup wrks
logger.Log('\tBacking up old data ...')
backupData(getQuestionDbs())
// -------------------------------------------------------------------------------------------------------
// adding questions to db
// -------------------------------------------------------------------------------------------------------
let totalNewQuestions = 0
let totalNewSubjects = 0
let totalNewQdbs = 0
for (let i = 0; i < resultData.length; i++) {
const { questionDbs: remoteQuestionDbs, peer } = resultData[i]
// FIXME: if remoteQuestionDbs contain multiple dbs with the same name, then the merging
// process could get wonky. Ideally it should not contain, but we will see
const { rawNewQuestionDbs, mergeResults } = await getMergeResults(
remoteQuestionDbs
)
const newQuestionDbs = updateQdbForLocalUse(rawNewQuestionDbs)
const { mergedQuestionDbs, changedQdbIndexes } = mergeQdbs(
getQuestionDbs(),
mergeResults
)
// TOTEST: test muliple new question dbs from multiple sources
// setting new index & path
writeNewData(
newQuestionDbs,
getQuestionDbs().filter((qdb) => {
return changedQdbIndexes.includes(qdb.index)
})
)
setQuestionDbs([...mergedQuestionDbs, ...newQuestionDbs])
const { newQuestionDbCount, newSubjectCount, newQuestionCount } =
await sendNewDataToWorkers(mergeResults, newQuestionDbs)
if (newQuestionCount > 0) {
logger.Log(
`\tAdded ${logger.C(
'green'
)}${newQuestionDbCount.toLocaleString()}${logger.C()} new question DB-s, ${logger.C(
'green'
)}${newSubjectCount.toLocaleString()}${logger.C()} new subjects and ${logger.C(
'green'
)}${newQuestionCount.toLocaleString()}${logger.C()} new questions from "${logger.C(
'blue'
)}${peerToString(peer)}${logger.C()}"`
)
}
// Processing result data is successfull
const newPeers = peers.map((x) => {
if (isPeerSameAs(peer, x)) {
return {
...x,
lastSync: syncStart,
}
} else {
return x
}
})
utils.WriteFile(JSON.stringify(newPeers, null, 2), peersFile)
totalNewQdbs += newQuestionDbCount
totalNewSubjects += newSubjectCount
totalNewQuestions += newQuestionCount
}
// -------------------------------------------------------------------------------------------------------
updateLastSync(selfInfo, syncStart)
const newQdb = getQuestionDbs()
const { subjCount: newSubjCount, questionCount: newQuestionCount } =
countOfQdbs(newQdb)
const newQuestionDbCount = newQdb.length
logger.logTable([
['\t', 'Users', 'QDBs', 'Subjs', 'Questions'],
[
'Old\t',
oldUserCount,
oldQuestionDbCount,
oldSubjCount,
oldQuestionCount,
],
[
'Added',
newUsers,
totalNewQdbs,
totalNewSubjects,
totalNewQuestions,
],
[
'Final',
newUserCount,
newQuestionDbCount,
newSubjCount,
newQuestionCount,
],
])
logger.Log(
`Question DB-s written! Sync successfully finished!`,
'green'
)
return {
old: {
oldUserCount: oldUserCount,
oldQuestionDbCount: oldQuestionDbCount,
oldSubjCount: oldSubjCount,
oldQuestionCount: oldQuestionCount,
},
added: {
totalNewQdbs: totalNewQdbs,
totalNewSubjects: totalNewSubjects,
totalNewQuestions: totalNewQuestions,
},
final: {
newUserCount: newUserCount,
newQuestionDbCount: newQuestionDbCount,
newSubjCount: newSubjCount,
newQuestionCount: newQuestionCount,
},
}
}
// ---------------------------------------------------------------------------------------
// APP SETUP
// ---------------------------------------------------------------------------------------
app.get('/p2pinfo', (req: Request, res: Response<RemotePeerInfo>) => {
logger.LogReq(req)
res.json(getSelfInfo(true))
})
app.get('/getnewdatasince', (req: Request, res: Response<SyncDataRes>) => {
// TODO: hash question db to see if different?
logger.LogReq(req)
const since = +req.query.since
const remoteHost = req.query.host
const questionDbsWithNewQuestions = Number.isNaN(since)
? questionDbs
: questionDbs
.map((qdb) => {
return {
...qdb,
data: getNewDataSince(qdb.data, since),
}
})
.filter((qdb) => {
const { questionCount: questionCount } = countOfQdb(qdb)
return questionCount > 0
})
const { subjCount: subjects, questionCount: questions } = countOfQdbs(
questionDbsWithNewQuestions
)
const result: SyncDataRes = {
questionDbs: questionDbsWithNewQuestions,
count: {
qdbs: questionDbsWithNewQuestions.length,
subjects: subjects,
questions: questions,
},
remoteInfo: getSelfInfo(),
}
if (remoteHost) {
const remoteHostInfo = peers.find((peer) => {
return peer.host === remoteHost
})
const remotePublicKey = remoteHostInfo?.publicKey
if (remotePublicKey) {
// FIXME: sign data?
const newUsers = getNewUsersSince(since)
result.encryptedUsers = encrypt(
remotePublicKey,
JSON.stringify(newUsers)
)
} else if (remoteHostInfo) {
logger.Log(
`Warning: ${remoteHostInfo.host}:${remoteHostInfo.port} has no publick key saved!`,
'yellowbg'
)
}
}
res.json(result)
})
app.get('/syncp2pdata', (req: Request, res: Response) => {
logger.LogReq(req)
// FINALIZE: uncomment
// const user = req.session.user
// if (user.id !== 1) {
// res.json({
// status: 'error',
// msg: 'only user 1 can call this EP',
// })
// return
// }
syncData()
.then((syncResult) => {
res.json({
msg: 'sync successfull',
...syncResult,
})
})
.catch((e) => {
console.error(e)
res.json({
error: e,
msg: e.message,
})
})
})
logger.Log('P2P functionality set up. Peers: ' + peers.length, 'blue')
return {}
}
export default {
setup: setup,
}

View file

@ -44,24 +44,21 @@ import {
Result,
backupData,
shouldSearchDataFile,
loadJSON,
writeData,
editDb,
RecievedData,
} from '../../../utils/actions'
import {
dataToString,
getSubjNameWithoutYear,
WorkerResult,
SearchResultQuestion,
// compareQuestionObj,
} from '../../../utils/classes'
import {
doALongTask,
msgAllWorker,
initWorkerPool,
} from '../../../utils/workerPool'
import { doALongTask, msgAllWorker } from '../../../utils/workerPool'
import dbtools from '../../../utils/dbtools'
import {
dataToString,
getSubjNameWithoutYear,
SearchResultQuestion,
} from '../../../utils/qdbUtils'
interface SavedQuestionData {
fname: string
@ -469,11 +466,15 @@ function getNewQdb(
}
function setup(data: SubmoduleData): Submodule {
const { app, userDB, /* url */ publicdirs /* moduleSpecificData */ } = data
const {
app,
userDB,
/* url */ publicdirs,
moduleSpecificData: { questionDbs: questionDbs, dbsFile: dbsFile },
} = data
const publicDir = publicdirs[0]
const motdFile = publicDir + 'motd'
const dbsFile = publicDir + 'questionDbs.json'
const savedQuestionsDir = publicDir + 'savedQuestions'
let version = LoadVersion()
@ -481,10 +482,6 @@ function setup(data: SubmoduleData): Submodule {
let motd = LoadMOTD(motdFile)
let testUsers: number[] = LoadTestUsers()
const dataFiles: Array<DataFile> = utils.ReadJSON(dbsFile)
const questionDbs: Array<QuestionDb> = loadJSON(dataFiles, publicDir)
initWorkerPool(() => questionDbs)
const filesToWatch = [
{
fname: motdFile,

View file

@ -320,7 +320,7 @@ function setup(data: SubmoduleData): Submodule {
}
)
function getDayDiff(dateString: string | Date) {
function getDayDiff(dateString: string | Date | number) {
const msdiff = new Date().getTime() - new Date(dateString).getTime()
return Math.floor(msdiff / (1000 * 3600 * 24))
}

View file

@ -24,7 +24,7 @@ console.log('Current working directory: ' + process.cwd())
const startHTTPS = true
const isRoot = process.getuid && process.getuid() === 0
const port = 8080
const port = process.env.PORT || 8080
const httpsport = 5001
// import os from 'os'

View file

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { countQuestionsInSubjects } = require('../../dist/utils/qdbUtils.js')
const fs = require('fs')
const command = process.argv[2]
const args = process.argv.slice(3)
const actions = {
qdbcount: () => {
const qdb = JSON.parse(fs.readFileSync(args[0], 'utf-8'))
const questionCount = countQuestionsInSubjects(qdb)
console.log({ questionCount: questionCount })
},
}
if (actions[command]) {
actions[command]()
} else {
console.log('No action for ' + command)
console.log('Possible commands: ', Object.keys(actions))
}

View file

@ -1,11 +1,11 @@
import { Subject, Question } from '../types/basicTypes'
import fs from 'fs'
import { RecievedData } from '../utils/actions'
import {
addQuestion,
createQuestion,
getSubjNameWithoutYear,
} from '../utils/classes'
import { Subject, Question } from '../types/basicTypes'
import fs from 'fs'
import { RecievedData } from '../utils/actions'
} from '../utils/qdbUtils'
const question: Question = createQuestion('asd', 'asd', { type: 'simple' })

View file

@ -1,58 +1,23 @@
import { updateQuestionsInArray } from '../utils/actions'
import { createQuestion } from '../utils/classes'
import { cleanDb } from '../utils/classes'
import { QuestionDb, Subject, Question } from '../types/basicTypes'
const date = (x?: number) => new Date().getTime() + (x || 0)
import { questions } from './testData'
import { cleanDb } from '../utils/qdbUtils'
const q1 = createQuestion(
'A kötvény és a részvény közös tulajdonsága, hogy TOREMOVE',
'piaci áruk eltérhet a névértéktől.',
{
type: 'simple',
date: date(-1000),
}
)
const q2 = createQuestion(
'A kötvény és a részvény közös tulajdonsága, hogy TOREMOVE',
'afjléa gféda gfdjs légf',
{
type: 'simple',
date: date(-1000),
}
)
const q3 = createQuestion(
'A kötvény és a részvény közös tulajdonsága, hogy TOREMOVE',
'afjlsd gfds dgfs gf sdgf d',
{
type: 'simple',
date: date(-1000),
}
)
const q4 = createQuestion(
'A kötvény névértéke',
'A kötvényen feltüntetett meghatározott nagyságú összeg.',
{
type: 'simple',
date: date(-1000),
}
)
const q5 = createQuestion(
'Mi az osztalék? asd asd',
'A vállalati profit egy része..',
{
type: 'simple',
date: date(1000),
}
)
const q6 = createQuestion(
'valaim nagyon értelmes kérdés asd asd',
'A vállalati profit egy része..',
{
type: 'simple',
date: date(1000),
}
)
const [q1, q2, q3, q4] = questions.slice(0, 4).map((q) => ({
...q,
data: {
...q.data,
date: 100,
},
}))
const [q5, q6] = questions.slice(4, 6).map((q) => ({
...q,
data: {
...q.data,
date: 1000,
},
}))
function setupTest({
newQuestions,
@ -68,12 +33,12 @@ function setupTest({
...x,
data: {
...x.data,
date: date(),
date: 500,
},
}
})
const subjName = subjToClean || 'subject'
const overwriteFromDate = date(-100)
const overwriteBeforeDate = 400
const qdbIndex = 0
const qdbs: QuestionDb[] = [
{
@ -93,7 +58,7 @@ function setupTest({
{
questions: recievedQuestions,
subjToClean: subjName,
overwriteFromDate: overwriteFromDate,
overwriteBeforeDate: overwriteBeforeDate,
qdbIndex: qdbIndex,
},
qdbs
@ -108,7 +73,7 @@ function setupTest({
return {
questionIndexesToRemove: questionIndexesToRemove,
updatedQuestions: updatedQuestions,
overwriteFromDate: overwriteFromDate,
overwriteBeforeDate: overwriteBeforeDate,
subjIndex: subjIndex,
}
}
@ -116,21 +81,16 @@ function setupTest({
const s1: Subject = { Name: 'test subject', Questions: [q1, q2, q4, q5] }
test('Old and duplicate questions should be removed from the database', () => {
const { questionIndexesToRemove, updatedQuestions, overwriteFromDate } =
setupTest({ newQuestions: [q1, q4, q5], data: [s1] })
expect(questionIndexesToRemove.length).toBe(3)
expect(questionIndexesToRemove[0].length).toBe(2)
expect(updatedQuestions.length).toBe(3)
const toremoveCount = updatedQuestions.filter((question) => {
return question.Q.includes('TOREMOVE')
}).length
expect(toremoveCount).toBe(1)
const newQuestion = updatedQuestions.find((question) => {
return question.Q.includes('TOREMOVE')
const { questionIndexesToRemove, updatedQuestions } = setupTest({
newQuestions: [q1, q2, q3],
data: [s1],
})
expect(newQuestion.data.date > overwriteFromDate).toBeTruthy()
expect(questionIndexesToRemove[0].length).toBe(2)
expect(questionIndexesToRemove[1].length).toBe(2)
expect(questionIndexesToRemove[2].length).toBe(2)
expect(updatedQuestions.length).toBe(5)
})
const s2: Subject = {
@ -139,30 +99,37 @@ const s2: Subject = {
}
test('Old and duplicate questions should be removed from the database round 2', () => {
const { questionIndexesToRemove, updatedQuestions, overwriteFromDate } =
setupTest({ newQuestions: [q1, q4, q5], data: [s2] })
const { questionIndexesToRemove, updatedQuestions } = setupTest({
newQuestions: [q1, q4, q5],
data: [s2],
})
expect(questionIndexesToRemove.length).toBe(3)
expect(questionIndexesToRemove[0].length).toBe(3)
expect(questionIndexesToRemove[1].length).toBe(1)
expect(questionIndexesToRemove[2].length).toBe(0)
expect(updatedQuestions.length).toBe(4)
const toremoveCount = updatedQuestions.filter((question) => {
return question.Q.includes('TOREMOVE')
}).length
expect(toremoveCount).toBe(1)
const newQuestion = updatedQuestions.find((question) => {
return question.Q.includes('TOREMOVE')
})
test('Old and duplicate questions should be removed from the database round 3', () => {
const { questionIndexesToRemove, updatedQuestions } = setupTest({
newQuestions: [q5, q6],
data: [s2],
})
expect(newQuestion.data.date > overwriteFromDate).toBeTruthy()
expect(questionIndexesToRemove[0].length).toBe(0)
expect(questionIndexesToRemove[1].length).toBe(0)
expect(updatedQuestions.length).toBe(6)
})
const s3: Subject = {
Name: 'test subject',
Questions: [q5, q6].map((x) => ({
...x,
Questions: [q5, q6].map((q) => ({
...q,
data: {
...x.data,
date: date(+50000),
...q.data,
date: 50000,
},
})),
}

204
src/tests/p2p.test.ts Normal file
View file

@ -0,0 +1,204 @@
import { getNewDataSince, mergeSubjects } from '../modules/api/submodules/p2p'
import {
countQuestionsInSubjects,
getSubjectDifference,
} from '../utils/qdbUtils'
// import { QuestionDb, Subject } from '../types/basicTypes'
import { questions, subjects } from './testData'
// ------------------------------------------------------------------------------------
// getSubjectDifference
// ------------------------------------------------------------------------------------
test('Merging two similar question dbs should result in the same', () => {
const { newData, newSubjects } = getSubjectDifference(subjects, subjects)
const newQuestionCount = countQuestionsInSubjects(newData)
expect(newSubjects.length).toBe(0)
expect(newQuestionCount).toBe(0)
})
test('Merging qdb with another that has one extra question should get added', () => {
const { newData, newSubjects } = getSubjectDifference(subjects, [
subjects[0],
{
...subjects[1],
Questions: [...subjects[1].Questions, questions[10]],
},
])
const newQuestionCount = countQuestionsInSubjects(newData)
expect(newSubjects.length).toBe(0)
expect(newQuestionCount).toBe(1)
})
test('Qdb merging adding new subject', () => {
const { newData, newSubjects } = getSubjectDifference(
[subjects[0]],
[subjects[0], subjects[1]]
)
const newQuestionCount = countQuestionsInSubjects(newData)
expect(newSubjects.length).toBe(1)
expect(newQuestionCount).toBe(0)
})
test('Qdb merging adding new subject and new questions', () => {
const { newData, newSubjects } = getSubjectDifference(
[subjects[0]],
[
{
...subjects[0],
Questions: [...subjects[0].Questions, questions[10]],
},
subjects[1],
subjects[2],
]
)
const newQuestionCount = countQuestionsInSubjects(newData)
expect(newSubjects.length).toBe(2)
expect(newQuestionCount).toBe(1)
})
// ------------------------------------------------------------------------------------
// getNewDataSince
// ------------------------------------------------------------------------------------
test('get new data since works', () => {
const [q1, q2, q3] = questions
.slice(0, 3)
.map((q) => ({ ...q, data: { ...q.data, date: 500 } }))
const [q4, q5, q6] = questions
.slice(3, 6)
.map((q) => ({ ...q, data: { ...q.data, date: 1000 } }))
const res = getNewDataSince(
[
{
Name: '1',
Questions: [q1, q2, q3, q4, q5, q6],
},
],
750
)
expect(res.length).toBe(1)
expect(res[0].Questions.length).toBe(3)
})
test('get new data since works, multiple subjects', () => {
const [q1, q2, q3] = questions
.slice(0, 3)
.map((q) => ({ ...q, data: { ...q.data, date: 500 } }))
const [q4, q5, q6] = questions
.slice(3, 6)
.map((q) => ({ ...q, data: { ...q.data, date: 1000 } }))
const res = getNewDataSince(
[
{
Name: '1',
Questions: [q1, q2, q3, q4, q5, q6],
},
{
Name: '2',
Questions: [q1, q2, q3, q4],
},
],
750
)
expect(res.length).toBe(2)
expect(res[0].Questions.length).toBe(3)
expect(res[1].Questions.length).toBe(1)
})
test('get new data since works, multiple subjects round 2', () => {
const [q1, q2, q3] = questions
.slice(0, 3)
.map((q) => ({ ...q, data: { ...q.data, date: 500 } }))
const [q4, q5, q6] = questions
.slice(3, 6)
.map((q) => ({ ...q, data: { ...q.data, date: 1000 } }))
const [q7, q8, q9] = questions
.slice(6, 9)
.map((q) => ({ ...q, data: { ...q.data, date: 500 } }))
const res = getNewDataSince(
[
{
Name: '1',
Questions: [q1, q2, q3, q4, q5, q6],
},
{
Name: '2',
Questions: [q7, q8, q9],
},
],
750
)
expect(res.length).toBe(1)
expect(res[0].Questions.length).toBe(3)
})
// ------------------------------------------------------------------------------------
// mergeQdbs
// ------------------------------------------------------------------------------------
test('merge subjects works', () => {
const [s1] = subjects
const res = mergeSubjects([s1], [s1], [s1])
expect(res.length).toBe(2)
expect(res[0].Questions.length).toBe(8)
})
test('merge subjects works, three new subjects', () => {
const [s1] = subjects
const res = mergeSubjects([s1], [s1], [s1, s1, s1])
expect(res.length).toBe(4)
expect(res[0].Questions.length).toBe(8)
expect(res[1].Questions.length).toBe(4)
expect(res[2].Questions.length).toBe(4)
expect(res[3].Questions.length).toBe(4)
})
test('merge subjects works, no new subjects, two subjects to merge', () => {
const [s1] = subjects
const res = mergeSubjects([s1], [s1, s1, s1, s1], [])
expect(res.length).toBe(1)
expect(res[0].Questions.length).toBe(20)
})
test('merge subjects works, merging a subject with different name gets ignored', () => {
const [s1, s2, s3] = subjects
const res = mergeSubjects([s1, s2], [s3], [])
expect(res.length).toBe(2)
expect(res[0].Questions.length).toBe(4)
expect(res[1].Questions.length).toBe(4)
})
test('merge subjects works, 2 subjects to 2, 1 new', () => {
const [s1, s2, s3] = subjects
const res = mergeSubjects([s1, s2], [s1, s2], [s3])
expect(res.length).toBe(3)
expect(res[0].Questions.length).toBe(8)
expect(res[1].Questions.length).toBe(8)
expect(res[2].Questions.length).toBe(4)
})
test('merge subjects works, no new data', () => {
const [s1, s2] = subjects
const res = mergeSubjects([s1, s2], [], [])
expect(res.length).toBe(2)
expect(res[0].Questions.length).toBe(4)
expect(res[1].Questions.length).toBe(4)
})

View file

@ -1,9 +1,9 @@
import {
setNoPossibleAnswersPenalties,
SearchResultQuestion,
noPossibleAnswerMatchPenalty,
} from '../utils/classes'
import { setNoPossibleAnswersPenalties } from '../utils/classes'
import { Question } from '../types/basicTypes'
import {
noPossibleAnswerMatchPenalty,
SearchResultQuestion,
} from '../utils/qdbUtils'
const matchPercent = 100

View file

@ -0,0 +1,38 @@
import { getAvailableQdbIndexes } from '../utils/qdbUtils'
import { emptyQdb } from './testData'
test('getAvailableQdbIndexes works, normal order', () => {
const qdbs = [0, 1, 2, 3, 4].map((x) => {
return { ...emptyQdb, index: x }
})
const [index] = getAvailableQdbIndexes(qdbs)
expect(index).toBe(5)
})
test('getAvailableQdbIndexes works, one missing', () => {
const qdbs = [0, 1, 2, 3, 5].map((x) => {
return { ...emptyQdb, index: x }
})
const [index] = getAvailableQdbIndexes(qdbs)
expect(index).toBe(4)
})
test('getAvailableQdbIndexes works, empty qdb', () => {
const [index] = getAvailableQdbIndexes([])
expect(index).toBe(0)
})
test('getAvailableQdbIndexes works, multiple count', () => {
const qdbs = [0, 1, 2, 3, 5].map((x) => {
return { ...emptyQdb, index: x }
})
let indexes = getAvailableQdbIndexes(qdbs)
expect(indexes.length).toBe(1)
expect(indexes[0]).toBe(4)
indexes = getAvailableQdbIndexes(qdbs, 5)
expect(indexes).toStrictEqual([4, 6, 7, 8, 9])
})

263
src/tests/testData.ts Normal file
View file

@ -0,0 +1,263 @@
import { QuestionDb } from '../types/basicTypes'
import { createQuestion } from '../utils/qdbUtils'
const rawQuestions = [
// 0
{
Q: 'A kötvény és a részvény közös tulajdonsága, hogy TOREMOVE',
A: 'piaci áruk eltérhet a névértéktől.',
data: {
type: 'simple',
date: 1678692844547,
},
},
// 1
{
Q: 'A kötvény és a részvény közös tulajdonsága, hogy TOREMOVE',
A: 'afjléa gféda gfdjs légf',
data: {
type: 'simple',
date: 1678692844547,
},
},
// 2
{
Q: 'A kötvény és a részvény közös tulajdonsága, hogy TOREMOVE',
A: 'afjlsd gfds dgfs gf sdgf d',
data: {
type: 'simple',
date: 1678692844547,
},
},
// 3
{
Q: 'A kötvény névértéke',
A: 'A kötvényen feltüntetett meghatározott nagyságú összeg.',
data: {
type: 'simple',
date: 1678692844547,
},
},
// 4
{
Q: 'Mi az osztalék? asd asd',
A: 'A vállalati profit egy része..',
data: {
type: 'simple',
date: 1678692844547,
},
},
// 5
{
Q: 'valaim nagyon értelmes kérdés asd asd',
A: 'A vállalati profit egy része..',
data: {
type: 'simple',
date: 1678692844547,
},
},
// 6
{
Q: 'A kötvény és a részvény közös tulajdonsága, hogy',
A: 'piaci áruk eltérhet a névértéktől.',
data: {
type: 'simple',
source: 'script',
date: 1252626725558,
},
},
// 7
{
Q: 'A kötvény és a részvény közös tulajdonsága, hogy',
A: 'afjléa gféda gfdjs légf',
data: {
type: 'simple',
source: 'script',
date: 1252626725558,
},
},
// 8
{
Q: 'A kötvény névértéke',
A: 'A kötvényen feltüntetett meghatározott nagyságú összeg.',
data: {
type: 'simple',
source: 'script',
date: 1252626725558,
},
},
// 9
{
Q: 'A részvényesnek joga van',
A: 'Mind a háromra feljogosít.',
data: {
type: 'simple',
source: 'script',
date: 1652636725558,
},
},
// 10
{
Q: 'Mi az osztalék?',
A: 'A vállalati profit egy része..',
data: {
type: 'simple',
source: 'script',
date: 1652636725559,
},
},
// 11
{
Q: 'Az alábbi értékpapírok közül melyik kizárólagos kibocsátója a hitelintézet?',
A: 'letéti jegy.',
data: {
type: 'simple',
source: 'script',
date: 1652636725559,
},
},
// 12
{
Q: 'Mely állítás nem igaz a kötvényre?',
A: 'az osztalék-számítás módját fel kell tüntetni az értékpapíron.',
data: {
type: 'simple',
source: 'script',
date: 1652636725559,
},
},
// 13
{
Q: 'Mely állítás nem igaz a kötvényre?',
A: 'tagsági jogot megtestesítő értékpapír.',
data: {
type: 'simple',
source: 'script',
date: 1652636725559,
},
},
{
Q: 'Az osztalék közvetlenül nem függ',
A: 'a részvénytársaság múltbeli költséggazdálkodásától.',
data: {
type: 'simple',
source: 'script',
date: 1652636725560,
},
},
{
Q: 'Ha a részvénytársaság az egyik tulajdonosától visszavásárolja a saját részvényeit, akkor ezzel',
A: 'átrendezi a vállalat tulajdonosi szerkezetét.',
data: {
type: 'simple',
source: 'script',
date: 1652636725560,
},
},
{
Q: 'Válassza ki az értékpapírokra vonatkozó helyes megállapítást!',
A: 'Vagyoni jogot megtestesítő forgalomképes okirat.',
data: {
type: 'simple',
source: 'script',
date: 1652636725560,
},
},
{
Q: '10. Válassza ki, hogy mely értékpapírtípus járul hozzá egy vállalat alaptőkéjéhez?',
A: 'Részesedési jogot megtestesítő értékpapír.',
data: {
type: 'simple',
source: 'script',
date: 1652636725560,
},
},
{
Q: 'When does the ~/.bashrc script run automatically?',
A: 'When a new terminal window is opened..',
data: {
type: 'simple',
source: 'script',
date: 1678692844546,
},
},
{
Q: 'A robot is ...',
A: '... a complex mechatronic system enabled with electronics, sensors, actuators and software, executing tasks with a certain degree of autonomy. It may be preprogrammed, teleoperated or carrying out computations to make decisions. .',
data: {
type: 'simple',
source: 'script',
date: 1678692844546,
},
},
{
Q: 'A robot is ...',
A: '... some sort of device, which has sensors those sensors the world, does some sort of computation, decides on an action, and then does that action based on the sensory input, which makes some change out in the world, outside its body. .',
data: {
type: 'simple',
source: 'script',
date: 1678692844546,
},
},
{
Q: 'A robot is ...',
A: '... an actuated mechanism programmable in two or more axes with a degree of autonomy, moving within its environment, to perform intended tasks. .',
data: {
type: 'simple',
source: 'script',
date: 1678692844546,
},
},
{
Q: 'ROS is the abbreviation of ...',
A: 'Robot Operating System .',
data: {
type: 'simple',
source: 'script',
date: 1678692844546,
},
},
{
Q: 'A robot is ...',
A: '... a machine—especially one programmable by a computer— capable of carrying out a complex series of actions automatically. Robots can be guided by an external control device or the control may be embedded within. Robots may be constructed on the lines of human form, but most robots are machines designed to perform a task with no regard to their aesthetics. .',
data: {
type: 'simple',
source: 'script',
date: 1678692844547,
},
},
{
Q: 'Complete the definition of a robot: A robot is a complex mechatronic system enabled with electronics, üres , actuators and software, executing tasks with a certain degree of üres . It may be preprogrammed, üres or carrying out computations to make üres .',
A: 'Complete the definition of a robot: A robot is a complex mechatronic system enabled with electronics, [sensors], actuators and software, executing tasks with a certain degree of [autonomy]. It may be preprogrammed, [teleoperated] or carrying out computations to make [decisions].',
data: {
type: 'simple',
source: 'script',
date: 1678692844547,
},
},
]
export const questions = rawQuestions.map((q) => createQuestion(q))
export const subjects = [
{
Name: 'Pénzügyek alapjai',
Questions: questions.slice(0, 4),
},
{
Name: 'Programming robots in ROS',
Questions: questions.slice(4, 8),
},
{
Name: 'Programming something',
Questions: questions.slice(8, 12),
},
]
export const emptyQdb: QuestionDb = {
index: 0,
data: [],
path: '',
name: '',
shouldSearch: '',
shouldSave: {},
}

View file

@ -19,15 +19,15 @@
------------------------------------------------------------------------- */
import express from 'express'
import { SearchResultQuestion } from '../utils/classes'
import type { Database } from 'better-sqlite3'
import type { Socket as SocketIoSocket } from 'socket.io'
import http from 'http'
import https from 'https'
import { SearchResultQuestion } from '../utils/qdbUtils'
export interface QuestionData {
type: string
date?: Date | number
date?: number
images?: Array<string>
hashedImages?: Array<string>
possibleAnswers?: Array<{
@ -36,6 +36,7 @@ export interface QuestionData {
selectedByUser?: boolean
}>
base64?: string[]
source?: string
}
export interface Question {
@ -59,7 +60,7 @@ export interface DataFile {
locked?: Boolean
overwrites?: Array<{
subjName: string
overwriteFromDate: number
overwriteBeforeDate: number
}>
shouldSearch:
| string
@ -87,16 +88,16 @@ export interface QuestionDb extends DataFile {
export interface User {
id: number
pw: string
created: Date
}
export interface User {
id: number
pw: string
pwRequestCount: number
avaiblePWRequests: number
oldCID?: number
notes?: string
loginCount: number
avaiblePWRequests: number
pwRequestCount: number
createdBy: number
created: number
lastLogin: number
lastAccess: number
sourceHost?: number
}
export interface Request<T = any> extends express.Request {
@ -111,13 +112,21 @@ export interface Request<T = any> extends express.Request {
query: { [key: string]: string }
}
export interface ModuleSpecificData {
// TODO: rename to something more meaningfull
questionDbs: QuestionDb[]
setQuestionDbs: (newVal: QuestionDb[]) => void
getQuestionDbs: () => QuestionDb[]
dbsFile: string
}
export interface SubmoduleData {
app: express.Application
url: string
publicdirs: Array<string>
userDB?: Database
nextdir?: string
moduleSpecificData?: any
moduleSpecificData: ModuleSpecificData
httpServer: http.Server
httpsServer: https.Server
}
@ -159,3 +168,11 @@ export interface ModuleType {
export interface Socket extends SocketIoSocket {
user: User
}
export interface PeerInfo {
name: string
host: string
port: number
publicKey: string
lastSync?: Date
}

5
src/types/typeSchemas.ts Normal file
View file

@ -0,0 +1,5 @@
export const PeerInfoSchema = {
name: { type: 'string' },
host: { type: 'string' },
port: { type: 'string' },
}

View file

@ -22,15 +22,16 @@ const recDataFile = './stats/recdata'
const dataLockFile = './data/lockData'
import logger from '../utils/logger'
import {
createQuestion,
WorkerResult,
SearchResultQuestion,
} from '../utils/classes'
import { WorkerResult } from '../utils/classes'
import { doALongTask, msgAllWorker } from './workerPool'
import idStats from '../utils/ids'
import utils from '../utils/utils'
import { addQuestion, getSubjNameWithoutYear } from './classes'
import {
addQuestion,
createQuestion,
getSubjNameWithoutYear,
SearchResultQuestion,
} from '../utils/qdbUtils'
// types
import {
@ -40,6 +41,7 @@ import {
User,
DataFile,
} from '../types/basicTypes'
import { countOfQdbs } from './qdbUtils'
// if a recievend question doesnt match at least this % to any other question in the db it gets
// added to db
@ -321,8 +323,11 @@ function runCleanWorker(
subjName: string,
qdb: QuestionDb
) {
// FIXME: clean worker should compare images too!
// see: classes.ts:1011
return
if (qdb.overwrites && qdb.overwrites.length) {
// check if subject needs to be updated, and qdb has overwriteFromDate
// check if subject needs to be updated, and qdb has overwriteBeforeDate
const overwrite = qdb.overwrites.find((x) => {
return subjName.toLowerCase().includes(x.subjName.toLowerCase())
})
@ -343,7 +348,7 @@ function runCleanWorker(
data: {
questions: recievedQuesitons,
subjToClean: subjName,
overwriteFromDate: overwrite.overwriteFromDate,
overwriteBeforeDate: overwrite.overwriteBeforeDate,
qdbIndex: qdb.index,
},
}).then(({ result: questionIndexesToRemove }) => {
@ -391,15 +396,13 @@ export function updateQuestionsInArray(
questions: Question[],
newQuestions: Question[]
): Question[] {
const indexesToRemove = questionIndexesToRemove.reduce((acc, x) => {
if (x.length > 1) {
return [...acc, ...x]
}
return acc
}, [])
if (newQuestions.length !== questionIndexesToRemove.length) {
throw new Error('newQuestions length ne questionIndexesToRemove length')
}
const indexesToRemove = questionIndexesToRemove.flat()
const newQuestionsToAdd: Question[] = newQuestions.filter((_q, i) => {
return questionIndexesToRemove[i].length > 1
return questionIndexesToRemove[i].length >= 1
})
return [
@ -407,7 +410,7 @@ export function updateQuestionsInArray(
return !indexesToRemove.includes(i)
}),
...newQuestionsToAdd.map((x) => {
x.data.date = new Date()
x.data.date = new Date().getTime()
return x
}),
]
@ -500,6 +503,10 @@ export function loadJSON(
const dataPath = dataDir + dataFile.path
if (!utils.FileExists(dataPath)) {
logger.Log(
`${dataPath} data file did not exist, created empty one!`,
'yellowbg'
)
utils.WriteFile(JSON.stringify([]), dataPath)
}
@ -520,14 +527,7 @@ export function loadJSON(
return acc
}, [])
let subjCount = 0
let questionCount = 0
res.forEach((qdb) => {
subjCount += qdb.data.length
qdb.data.forEach((subj) => {
questionCount += subj.Questions.length
})
})
const { subjCount, questionCount } = countOfQdbs(res)
logger.Log(
`Loaded ${subjCount} subjects with ${questionCount} questions from ${res.length} question db-s`,
logger.GetColor('green')
@ -543,6 +543,7 @@ export function writeData(data: Array<Subject>, path: string): void {
return {
Name: subj.Name,
Questions: subj.Questions.map((question) => {
// removing cache here
return {
Q: question.Q,
A: question.A,

File diff suppressed because it is too large Load diff

View file

@ -93,16 +93,13 @@ function DebugLog(msg: string) {
}
}
// FIXME: this might not work: what is col exactly, and how we use AddColumn?
function AddColumn(
db: Database,
table: string,
col: { [key: string]: string | number }
colName: string,
colType: string
): RunResult {
try {
const colName = Object.keys(col)[0]
const colType = col.type
const command = `ALTER TABLE ${table} ADD COLUMN ${colName} ${colType}`
const stmt = PrepareStatement(db, command)

53
src/utils/encryption.ts Normal file
View file

@ -0,0 +1,53 @@
/* ----------------------------------------------------------------------------
Question Server
GitLab: <https://gitlab.com/MrFry/mrfrys-node-server>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
------------------------------------------------------------------------- */
import { Crypt, RSA } from 'hybrid-crypto-js'
const rsa = new RSA()
const crypt = new Crypt()
export const createKeyPair = (): Promise<{
publicKey: string
privateKey: string
}> => {
return rsa.generateKeyPairAsync()
}
export const encrypt = (publicKey: string, text: string): string => {
return crypt.encrypt(publicKey, text)
}
export const decrypt = (privateKey: string, text: string): string => {
return crypt.decrypt(privateKey, text).message
}
export const isKeypairValid = (
publicKey: string,
privateKey: string
): boolean => {
const testText = 'nem volt jobb ötletem na'
try {
const encryptedText = encrypt(publicKey, testText)
const decryptedText = decrypt(privateKey, encryptedText)
return decryptedText === testText
} catch (e) {
return false
}
}

View file

@ -389,6 +389,9 @@ function C(color?: string): string {
if (color === 'redbg') {
return '\x1b[41m'
}
if (color === 'yellowbg') {
return '\x1b[43m\x1b[30m'
}
if (color === 'bluebg') {
return '\x1b[44m'
}
@ -416,6 +419,23 @@ function C(color?: string): string {
return '\x1b[0m'
}
function logTable(table: (string | number)[][]): void {
table.forEach((row, i) => {
const rowString: string[] = []
row.forEach((cell, j) => {
const cellColor = j === 0 || i === 0 ? 'blue' : 'green'
let cellVal = ''
if (!isNaN(+cell)) {
cellVal = cell.toLocaleString()
} else {
cellVal = cell.toString()
}
rowString.push(C(cellColor) + cellVal + C())
})
Log(rowString.join('\t'))
})
}
export default {
getColoredDateString: getColoredDateString,
Log: Log,
@ -431,4 +451,5 @@ export default {
logDir: logDir,
vlogDir: vlogDir,
setLoggingDisabled: setLoggingDisabled,
logTable: logTable,
}

606
src/utils/qdbUtils.ts Normal file
View file

@ -0,0 +1,606 @@
import logger from './logger'
import {
Question,
QuestionData,
QuestionDb,
Subject,
} from '../types/basicTypes'
interface DetailedMatch {
qMatch: number
aMatch: number
dMatch: number
matchedSubjName: string
avg: number
}
export interface SearchResultQuestion {
q: Question
match: number
detailedMatch: DetailedMatch
}
/* Percent minus for length difference */
const lengthDiffMultiplier = 10
// const commonUselessStringParts = [',', '\\.', ':', '!', '\\+', '\\s*\\.']
/* Minimum ammount to consider that two questions match during answering */
const minMatchAmmount = 75
const magicNumber = 0.7 // same as minMatchAmmount, but /100
export const minMatchToNotSearchOtherSubjects = 90
/* If all of the results are below this match percent (when only one subject is searched due to
* subject name matching) then all subjects are searched for answer */
export const noPossibleAnswerMatchPenalty = 5
const commonUselessAnswerParts = [
'A helyes válasz az ',
'A helyes válasz a ',
'A helyes válaszok: ',
'A helyes válaszok:',
'A helyes válasz: ',
'A helyes válasz:',
'The correct answer is:',
"'",
]
export function getSubjNameWithoutYear(subjName: string): string {
const t = subjName.split(' - ')
if (t[0].match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{1}$/i)) {
return t[1] || subjName
} else {
return subjName
}
}
function simplifyString(toremove: string): string {
return toremove.replace(/\s/g, ' ').replace(/\s+/g, ' ').toLowerCase()
}
function removeStuff(
value: string,
removableStrings: Array<string>,
toReplace?: string
): string {
removableStrings.forEach((removableString) => {
const regex = new RegExp(removableString, 'g')
value = value.replace(regex, toReplace || '')
})
return value
}
// damn nonbreaking space
function normalizeSpaces(input: string): string {
return input.replace(/\s/g, ' ')
}
function removeUnnecesarySpaces(toremove: string): string {
return normalizeSpaces(toremove)
.replace(/\s+/g, ' ')
.replace(/(\r\n|\n|\r)/gm, '')
.trim()
}
export function compareString(
s1: string,
s2: string,
s1cache?: Array<string>,
s2cache?: Array<string>
): number {
const s1a = s1cache || s1.split(' ')
const s2a = s2cache || s2.split(' ')
if (s1 === s2) {
return 100
}
if (!s1a || !s2a) {
if (!s1a && !s2a) {
return 100
} else {
return 0
}
}
if (s1a.length < 0 || s2a.length < 0) {
if (s1a.length === 0 && s2a.length === 0) {
return 100
} else {
return 0
}
}
let match = 0
let lastMatchIndex = -2
let i = 0
while (i < s1a.length) {
if (match / i < magicNumber) {
break
}
const currMatchIndex = s2a.indexOf(s1a[i])
if (currMatchIndex !== -1 && lastMatchIndex < currMatchIndex) {
match++
lastMatchIndex = currMatchIndex
}
i++
}
let percent = Math.round(
parseFloat(((match / s1a.length) * 100).toFixed(2))
)
const lengthDifference = Math.abs(s2a.length - s1a.length)
percent -= lengthDifference * lengthDiffMultiplier
if (percent < 0) {
percent = 0
}
return percent
}
function answerPreProcessor(value: string): string {
if (!value) {
return value
}
return removeStuff(value, commonUselessAnswerParts)
}
// 'a. pécsi sör' -> 'pécsi sör'
function removeAnswerLetters(value: string): string {
if (!value) {
return value
}
const val = value.split('. ')
if (val[0].length < 2 && val.length > 1) {
val.shift()
return val.join(' ')
} else {
return value
}
}
function simplifyQA(value: string, mods: Array<Function>): string {
if (!value) {
return value
}
return mods.reduce((res, fn) => {
return fn(res)
}, value)
}
function simplifyAnswer(value: string): string {
if (!value) {
return value
}
return simplifyQA(value, [
removeUnnecesarySpaces,
answerPreProcessor,
removeAnswerLetters,
])
}
export function simplifyQuestion(question: string): string {
if (!question) {
return question
}
return simplifyQA(question, [removeUnnecesarySpaces, removeAnswerLetters])
}
function simplifyQuestionObj(question: Question): Question {
if (!question) {
return question
}
if (question.Q) {
question.Q = simplifyQA(question.Q, [
removeUnnecesarySpaces,
removeAnswerLetters,
])
}
if (question.A) {
question.A = simplifyQA(question.A, [
removeUnnecesarySpaces,
removeAnswerLetters,
])
}
return question
}
export function createQuestion(
question: Question | string,
answer?: string,
data?: QuestionData
): Question {
try {
if (typeof question === 'string') {
return {
Q: simplifyQuestion(question),
A: answer ? simplifyAnswer(answer) : undefined,
data: data,
cache: {
Q: question ? simplifyString(question).split(' ') : [],
A: answer ? simplifyString(answer).split(' ') : [],
},
}
} else {
return {
...question,
cache: {
Q: question.Q ? simplifyString(question.Q).split(' ') : [],
A: question.A ? simplifyString(question.A).split(' ') : [],
},
}
}
} catch (err) {
logger.Log('Error creating question', logger.GetColor('redbg'))
console.error(question, answer, data)
console.error(err)
return null
}
}
function compareImage(data: QuestionData, data2: QuestionData): number {
if (data.hashedImages && data2.hashedImages) {
return compareString(
data.hashedImages.join(' '),
data2.hashedImages.join(' '),
data.hashedImages,
data2.hashedImages
)
} else if (data.images && data2.images) {
return (
compareString(
data.images.join(' '),
data2.images.join(' '),
data.images,
data2.images
) - 10
)
} else {
return 0
}
}
function compareData(q1: Question, q2: Question): number {
try {
if (q1.data.type === q2.data.type) {
const dataType = q1.data.type
if (dataType === 'simple') {
return -1
} else if (dataType === 'image') {
return compareImage(q1.data, q2.data)
} else {
logger.DebugLog(
`Unhandled data type ${dataType}`,
'Compare question data',
1
)
logger.DebugLog(q1, 'Compare question data', 2)
}
} else {
return 0
}
} catch (error) {
logger.DebugLog('Error comparing data', 'Compare question data', 1)
logger.DebugLog(error.message, 'Compare question data', 1)
logger.DebugLog(error, 'Compare question data', 2)
console.error(error)
}
return 0
}
function compareQuestion(q1: Question, q2: Question): number {
return compareString(q1.Q, q2.Q, q1.cache.Q, q2.cache.Q)
// return compareString(
// q1.Q,
// q1.Q ? q1.Q.split(' ') : [],
// q2.Q,
// q2.Q ? q2.Q.split(' ') : []
// )
}
function compareAnswer(q1: Question, q2: Question): number {
return compareString(q1.A, q2.A, q1.cache.A, q2.cache.A)
// return compareString(
// q1.A,
// q1.A ? q1.A.split(' ') : [],
// q2.A,
// q2.A ? q2.A.split(' ') : []
// )
}
export function compareQuestionObj(
q1: Question,
_q1subjName: string,
q2: Question,
q2subjName: string
): DetailedMatch {
const qMatch = compareQuestion(q1, q2)
const aMatch = q2.A ? compareAnswer(q1, q2) : 0
// -1 if botth questions are simple
const dMatch = compareData(q1, q2)
let avg = -1
if (q2.A) {
if (dMatch === -1) {
avg = Math.min(qMatch, aMatch)
} else {
avg = Math.min(qMatch, aMatch, dMatch)
}
} else {
if (dMatch === -1) {
avg = qMatch
} else {
avg = Math.min(qMatch, dMatch)
}
}
return {
qMatch: qMatch,
aMatch: aMatch,
dMatch: dMatch,
matchedSubjName: q2subjName,
avg: avg,
}
}
function questionToString(question: Question): string {
const { Q, A, data } = question
if (data.type !== 'simple') {
return '?' + Q + '\n!' + A + '\n>' + JSON.stringify(data)
} else {
return '?' + Q + '\n!' + A
}
}
function subjectToString(subj: Subject): string {
const { Questions, Name } = subj
const result: string[] = []
Questions.forEach((question) => {
result.push(questionToString(question))
})
return '+' + Name + '\n' + result.join('\n')
}
export function addQuestion(
data: Array<Subject>,
subj: string,
question: Question
): void {
logger.DebugLog('Adding new question with subjName: ' + subj, 'qdb add', 1)
logger.DebugLog(question, 'qdb add', 3)
const i = data.findIndex((subject) => {
return (
subject.Name &&
subj
.toLowerCase()
.includes(getSubjNameWithoutYear(subject.Name).toLowerCase())
)
})
if (i !== -1) {
logger.DebugLog('Adding new question to existing subject', 'qdb add', 1)
data[i].Questions.push(question)
} else {
logger.Log(`Creating new subject: "${subj}"`)
data.push({
Name: subj,
Questions: [question],
})
}
}
export function prepareQuestion(question: Question): Question {
return simplifyQuestionObj(createQuestion(question))
}
export function dataToString(data: Array<Subject>): string {
const result: string[] = []
data.forEach((subj) => {
result.push(subjectToString(subj))
})
return result.join('\n\n')
}
export function countQuestionsInSubject(subject: Subject): number {
return subject.Questions.length
}
export function countQuestionsInSubjects(subject: Subject[]): number {
let questionCount = 0
subject.forEach((subj) => {
questionCount += countQuestionsInSubject(subj)
})
return questionCount
}
export function countOfQdb(qdb: QuestionDb): {
subjCount: number
questionCount: number
} {
const subjCount = qdb.data.length
const questionCount = countQuestionsInSubjects(qdb.data)
return { subjCount: subjCount, questionCount: questionCount }
}
export function countOfQdbs(qdbs: QuestionDb[]): {
subjCount: number
questionCount: number
} {
let questionCount = 0
let subjCount = 0
qdbs.forEach((qdb) => {
const { subjCount: sc, questionCount: qc } = countOfQdb(qdb)
questionCount += qc
subjCount += sc
})
return { subjCount: subjCount, questionCount: questionCount }
}
export function searchSubject(
subj: Subject,
question: Question,
subjName: string,
searchTillMatchPercent?: number
): SearchResultQuestion[] {
let result: SearchResultQuestion[] = []
let stopSearch = false
let i = subj.Questions.length - 1
while (i >= 0 && !stopSearch) {
const currentQuestion = subj.Questions[i]
const percent = compareQuestionObj(
currentQuestion,
subjName,
question,
subj.Name
)
if (percent.avg >= minMatchAmmount) {
result.push({
q: currentQuestion,
match: percent.avg,
detailedMatch: percent,
})
}
if (searchTillMatchPercent && percent.avg >= searchTillMatchPercent) {
stopSearch = true
}
i--
}
result = result.sort((q1, q2) => {
if (q1.match < q2.match) {
return 1
} else if (q1.match > q2.match) {
return -1
} else {
return 0
}
})
return result
}
export function getSubjectDifference(
subjects: Subject[],
subjectsToMerge: Subject[]
): { newData: Subject[]; newSubjects: Subject[] } {
const newData: Subject[] = []
const newSubjects: Subject[] = []
subjectsToMerge.forEach((remoteSubj) => {
const localSubj = subjects.find((ls) => ls.Name === remoteSubj.Name)
if (!localSubj) {
newSubjects.push(remoteSubj)
return
}
const addedQuestions: Question[] = []
remoteSubj.Questions.forEach((remoteQuestion) => {
const searchResult = searchSubject(
localSubj,
remoteQuestion,
localSubj.Name,
95 // FIXME: maybe fine tune
)
if (searchResult.length === 0) {
addedQuestions.push(remoteQuestion)
}
})
if (addedQuestions.length > 0) {
newData.push({
Name: localSubj.Name,
Questions: addedQuestions,
})
}
})
return { newData: newData, newSubjects: newSubjects }
}
export function cleanDb(
{
questions: recievedQuestions,
subjToClean,
overwriteBeforeDate,
qdbIndex,
}: {
questions: Question[]
subjToClean: string
overwriteBeforeDate: number
qdbIndex: number
},
qdbs: QuestionDb[]
): number[][] {
const subjIndex = qdbs[qdbIndex].data.findIndex((x) => {
return x.Name.toLowerCase().includes(subjToClean.toLowerCase())
})
if (!qdbs[qdbIndex].data[subjIndex]) {
return recievedQuestions.map(() => [])
}
// FIXME: compare images & data too!
const questionIndexesToRemove = recievedQuestions.map((recievedQuestion) =>
qdbs[qdbIndex].data[subjIndex].Questions.reduce<number[]>(
(acc, question, i) => {
const res = compareString(
simplifyQuestion(recievedQuestion.Q),
simplifyQuestion(question.Q)
)
if (
res > minMatchToNotSearchOtherSubjects &&
(!question.data.date ||
question.data.date < overwriteBeforeDate)
) {
// questions indexes in subject, that should be
// removed because of recievedQuestion
return [...acc, i]
}
return acc
},
[]
)
)
return questionIndexesToRemove
}
export function removeCacheFromQuestion(question: Question): Question {
if (question.cache) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cache, ...questionWithoutCache } = question
return questionWithoutCache
} else {
return question
}
}
export function getAvailableQdbIndexes(
qdbs: QuestionDb[],
count = 1,
initialIndex?: number
): number[] {
const indexes = qdbs.map((x) => x.index)
const availableIndexes: number[] = []
const minCount = count < 1 ? 1 : count
let i = initialIndex || 0
while (availableIndexes.length < minCount) {
if (!indexes.includes(i)) {
availableIndexes.push(i)
}
i += 1
}
return availableIndexes
}

View file

@ -36,6 +36,7 @@ interface WorkerObj {
free: Boolean
}
// FIXME: type depending on type
export interface TaskObject {
type:
| 'work'
@ -44,6 +45,7 @@ export interface TaskObject {
| 'newdb'
| 'dbClean'
| 'rmQuestions'
| 'merge'
data:
| {
searchIn: number[]
@ -57,11 +59,11 @@ export interface TaskObject {
}
| { dbIndex: number; edits: Edits }
| QuestionDb
| Result
| Omit<Result, 'qdbName'>
| {
questions: Question[]
subjToClean: string
overwriteFromDate: number
overwriteBeforeDate: number
qdbIndex: number
}
| {
@ -70,6 +72,10 @@ export interface TaskObject {
qdbIndex: number
recievedQuestions: Question[]
}
| {
localQdbIndex: number
remoteQdb: QuestionDb
}
}
interface PendingJob {
@ -90,7 +96,7 @@ interface DoneEvent extends EventEmitter {
emit(event: 'done', res: WorkerResult): boolean
}
const alertOnPendingCount = 50
const alertOnPendingCount = 100
const workerFile = './src/utils/classes.ts'
let workers: Array<WorkerObj>
let getInitData: () => Array<QuestionDb> = null
@ -136,11 +142,10 @@ export function doALongTask(
targetWorkerIndex?: number
): Promise<WorkerResult> {
if (Object.keys(pendingJobs).length > alertOnPendingCount) {
logger.Log(
console.error(
`More than ${alertOnPendingCount} callers waiting for free resource! (${
Object.keys(pendingJobs).length
})`,
logger.GetColor('redbg')
})`
)
}