jexidb 1.0.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Database.mjs DELETED
@@ -1,376 +0,0 @@
1
- import { EventEmitter } from 'events'
2
- import FileHandler from './FileHandler.mjs'
3
- import IndexManager from './IndexManager.mjs'
4
- import Serializer from './Serializer.mjs'
5
- import { Mutex } from 'async-mutex'
6
- import fs from 'fs'
7
-
8
- export class Database extends EventEmitter {
9
- constructor(file, opts = {}) {
10
- super()
11
- this.opts = Object.assign({
12
- v8: false,
13
- index: { data: {} },
14
- indexes: {},
15
- compress: false,
16
- compressIndex: false,
17
- maxMemoryUsage: 64 * 1024 // 64KB
18
- }, opts)
19
- this.offsets = []
20
- this.shouldSave = false
21
- this.serializer = new Serializer(this.opts)
22
- this.fileHandler = new FileHandler(file)
23
- this.indexManager = new IndexManager(this.opts)
24
- this.indexOffset = 0
25
- this.writeBuffer = []
26
- this.mutex = new Mutex()
27
- }
28
-
29
- use(plugin) {
30
- if (this.destroyed) throw new Error('Database is destroyed')
31
- plugin(this)
32
- }
33
-
34
- async init() {
35
- if (this.destroyed) throw new Error('Database is destroyed')
36
- if (this.initialized) return
37
- if (this.initlializing) return await new Promise(resolve => this.once('init', resolve))
38
- this.initializing = true
39
- try {
40
- if (this.opts.clear) {
41
- await this.fileHandler.truncate(0).catch(console.error)
42
- throw new Error('Cleared, empty file')
43
- }
44
- const lastLine = await this.fileHandler.readLastLine()
45
- if (!lastLine || !lastLine.length) {
46
- throw new Error('File does not exists or is a empty file')
47
- }
48
- const offsets = await this.serializer.deserialize(lastLine, { compress: this.opts.compressIndex })
49
- if (!Array.isArray(offsets)) {
50
- throw new Error('File to parse offsets, expected an array')
51
- }
52
- this.indexOffset = offsets[offsets.length - 2]
53
- this.offsets = offsets
54
- const ptr = this.locate(offsets.length - 2)
55
- this.offsets = this.offsets.slice(0, -2)
56
- this.shouldTruncate = true
57
- let indexLine = await this.fileHandler.readRange(...ptr)
58
- const index = await this.serializer.deserialize(indexLine, { compress: this.opts.compressIndex })
59
- index && this.indexManager.load(index)
60
- } catch (e) {
61
- if (Array.isArray(this.offsets)) {
62
- this.offsets = []
63
- }
64
- this.indexOffset = 0
65
- if (!String(e).includes('empty file')) {
66
- console.error('Error loading database:', e)
67
- }
68
- } finally {
69
- this.initializing = false
70
- this.initialized = true
71
- this.emit('init')
72
- }
73
- }
74
-
75
- async save() {
76
- if (this.destroyed) throw new Error('Database is destroyed')
77
- if (!this.initialized) throw new Error('Database not initialized')
78
- if (this.saving) return new Promise(resolve => this.once('save', resolve))
79
- this.saving = true
80
- await this.flush()
81
- if (!this.shouldSave) return
82
- this.emit('before-save')
83
- const index = Object.assign({ data: {} }, this.indexManager.index)
84
- for (const field in this.indexManager.index.data) {
85
- for (const term in this.indexManager.index.data[field]) {
86
- index.data[field][term] = [...this.indexManager.index.data[field][term]] // set to array
87
- }
88
- }
89
- const offsets = this.offsets.slice(0)
90
- const indexString = await this.serializer.serialize(index, { compress: this.opts.compressIndex, linebreak: true }) // force linebreak here to allow 'init' to read last line as offsets correctly
91
- for (const field in this.indexManager.index.data) {
92
- for (const term in this.indexManager.index.data[field]) {
93
- this.indexManager.index.data[field][term] = new Set(index.data[field][term]) // set back to set because of serialization
94
- }
95
- }
96
- offsets.push(this.indexOffset)
97
- offsets.push(this.indexOffset + indexString.length)
98
- // save offsets as JSON always to prevent linebreaks on last line, which breaks 'init()'
99
- const offsetsString = await this.serializer.serialize(offsets, { json: true, compress: false, linebreak: false })
100
- this.writeBuffer.push(indexString)
101
- this.writeBuffer.push(offsetsString)
102
- await this.flush() // write the index and offsets
103
- this.shouldTruncate = true
104
- this.shouldSave = false
105
- this.saving = false
106
- this.emit('save')
107
- }
108
-
109
- async ready() {
110
- if (!this.initialized) {
111
- await new Promise(resolve => this.once('init', resolve))
112
- }
113
- }
114
-
115
- locate(n) {
116
- if (this.offsets[n] === undefined) {
117
- if (this.offsets[n - 1]) {
118
- return [this.indexOffset, Number.MAX_SAFE_INTEGER]
119
- }
120
- return
121
- }
122
- let end = (this.offsets[n + 1] || this.indexOffset || Number.MAX_SAFE_INTEGER)
123
- return [this.offsets[n], end]
124
- }
125
-
126
- getRanges(map) {
127
- return (map || Array.from(this.offsets.keys())).map(n => {
128
- const ret = this.locate(n)
129
- if (ret !== undefined) return { start: ret[0], end: ret[1], index: n }
130
- }).filter(n => n !== undefined)
131
- }
132
-
133
- async readLines(map, ranges) {
134
- if (!ranges) ranges = this.getRanges(map)
135
- const results = await this.fileHandler.readRanges(ranges, this.serializer.deserialize.bind(this.serializer))
136
- let i = 0
137
- for (const start in results) {
138
- if (!results[start] || results[start]._ !== undefined) continue
139
- while (this.offsets[i] != start && i < map.length) i++ // weak comparison as 'start' is a string
140
- results[start]._ = map[i++]
141
- }
142
- return Object.values(results).filter(r => r !== undefined)
143
- }
144
-
145
- async insert(data) {
146
- if (this.destroyed) throw new Error('Database is destroyed')
147
- if (!this.initialized) await this.init()
148
- if (this.shouldTruncate) {
149
- this.writeBuffer.push(this.indexOffset)
150
- this.shouldTruncate = false
151
- }
152
- const line = await this.serializer.serialize(data, { compress: this.opts.compress, v8: this.opts.v8 }) // using Buffer for offsets accuracy
153
- const position = this.offsets.length
154
- this.offsets.push(this.indexOffset)
155
- this.indexOffset += line.length
156
- this.emit('insert', data, position)
157
- this.writeBuffer.push(line)
158
- if (!this.flushing && this.currentWriteBufferSize() > this.opts.maxMemoryUsage) {
159
- await this.flush()
160
- }
161
- this.indexManager.add(data, position)
162
- this.shouldSave = true
163
- }
164
-
165
- currentWriteBufferSize() {
166
- const lengths = this.writeBuffer.filter(b => Buffer.isBuffer(b)).map(b => b.length)
167
- return lengths.reduce((a, b) => a + b, 0)
168
- }
169
-
170
- flush() {
171
- if (this.flushing) {
172
- return this.flushing
173
- }
174
- return this.flushing = new Promise((resolve, reject) => {
175
- if (this.destroyed) return reject(new Error('Database is destroyed'))
176
- if (!this.writeBuffer.length) return resolve()
177
- let err
178
- this._flush().catch(e => err = e).finally(() => {
179
- err ? reject(err) : resolve()
180
- this.flushing = false
181
- })
182
- })
183
- }
184
-
185
- async _flush() {
186
- const release = await this.mutex.acquire()
187
- let fd = await fs.promises.open(this.fileHandler.file, 'a')
188
- try {
189
- while (this.writeBuffer.length) {
190
- let data
191
- const pos = this.writeBuffer.findIndex(b => typeof b === 'number')
192
- if (pos === 0) {
193
- await fd.close()
194
- await this.fileHandler.truncate(this.writeBuffer.shift())
195
- fd = await fs.promises.open(this.fileHandler.file, 'a')
196
- continue
197
- } else if (pos === -1) {
198
- data = Buffer.concat(this.writeBuffer)
199
- this.writeBuffer.length = 0
200
- } else {
201
- data = Buffer.concat(this.writeBuffer.slice(0, pos))
202
- this.writeBuffer.splice(0, pos)
203
- }
204
- await fd.write(data)
205
- }
206
- this.shouldSave = true
207
- } catch (err) {
208
- console.error('Error flushing:', err)
209
- } finally {
210
- let err
211
- await fd.close().catch(e => err = e)
212
- release()
213
- err && console.error('Error closing file:', err)
214
- }
215
- }
216
-
217
- async *walk(map, options = {}) {
218
- if (this.destroyed) throw new Error('Database is destroyed')
219
- if (!this.initialized) await this.init()
220
- this.shouldSave && await this.save().catch(console.error)
221
- if (this.indexOffset === 0) return
222
- if (!Array.isArray(map)) {
223
- if (map instanceof Set) {
224
- map = [...map]
225
- } else if (map && typeof map === 'object') {
226
- map = [...this.indexManager.query(map, options)]
227
- } else {
228
- map = [...Array(this.offsets.length).keys()]
229
- }
230
- }
231
- const ranges = this.getRanges(map)
232
- const groupedRanges = await this.fileHandler.groupedRanges(ranges)
233
- const fd = await fs.promises.open(this.fileHandler.file, 'r')
234
- try {
235
- for (const groupedRange of groupedRanges) {
236
- for await (const row of this.fileHandler.readGroupedRange(groupedRange, fd)) {
237
- const entry = await this.serializer.deserialize(row.line, { compress: this.opts.compress, v8: this.opts.v8 })
238
- if (entry === null) continue
239
- if (options.includeOffsets) {
240
- yield { entry, start: row.start }
241
- } else {
242
- yield entry
243
- }
244
- }
245
- }
246
- } finally {
247
- await fd.close()
248
- }
249
- }
250
-
251
- async query(criteria, options = {}) {
252
- if (this.destroyed) throw new Error('Database is destroyed')
253
- if (!this.initialized) await this.init()
254
- this.shouldSave && await this.save().catch(console.error)
255
- if (Array.isArray(criteria)) {
256
- let results = await this.readLines(criteria)
257
- if (options.orderBy) {
258
- const [field, direction = 'asc'] = options.orderBy.split(' ')
259
- results.sort((a, b) => {
260
- if (a[field] > b[field]) return direction === 'asc' ? 1 : -1
261
- if (a[field] < b[field]) return direction === 'asc' ? -1 : 1
262
- return 0;
263
- })
264
- }
265
- if (options.limit) {
266
- results = results.slice(0, options.limit);
267
- }
268
- return results
269
- } else {
270
- const matchingLines = await this.indexManager.query(criteria, options)
271
- if (!matchingLines || !matchingLines.size) {
272
- return []
273
- }
274
- return await this.query([...matchingLines], options)
275
- }
276
- }
277
-
278
- async update(criteria, data, options={}) {
279
- if (this.shouldTruncate) {
280
- this.writeBuffer.push(this.indexOffset)
281
- this.shouldTruncate = false
282
- }
283
- if(this.destroyed) throw new Error('Database is destroyed')
284
- if(!this.initialized) await this.init()
285
- this.shouldSave && await this.save().catch(console.error)
286
- const matchingLines = await this.indexManager.query(criteria, options)
287
- if (!matchingLines || !matchingLines.size) {
288
- return []
289
- }
290
- const ranges = this.getRanges([...matchingLines])
291
- const validMatchingLines = new Set(ranges.map(r => r.index))
292
- if (!validMatchingLines.size) {
293
- return []
294
- }
295
- const entries = await this.readLines([...validMatchingLines], ranges)
296
- const lines = []
297
- for(const entry of entries) {
298
- let err
299
- const updated = Object.assign(entry, data)
300
- const ret = await this.serializer.serialize(updated).catch(e => err = e)
301
- err || lines.push(ret)
302
- }
303
- const offsets = []
304
- let byteOffset = 0, k = 0
305
- this.offsets.forEach((n, i) => {
306
- const prevByteOffset = byteOffset
307
- if (validMatchingLines.has(i) && ranges[k]) {
308
- const r = ranges[k]
309
- byteOffset += lines[k].length - (r.end - r.start)
310
- k++
311
- }
312
- offsets.push(n + prevByteOffset)
313
- })
314
- this.offsets = offsets
315
- this.indexOffset += byteOffset
316
- await this.fileHandler.replaceLines(ranges, lines);
317
- [...validMatchingLines].forEach((lineNumber, i) => {
318
- this.indexManager.dryRemove(lineNumber)
319
- this.indexManager.add(entries[i], lineNumber)
320
- })
321
- this.shouldSave = true
322
- return entries
323
- }
324
-
325
- async delete(criteria, options = {}) {
326
- if (this.shouldTruncate) {
327
- this.writeBuffer.push(this.indexOffset)
328
- this.shouldTruncate = false
329
- }
330
- if (this.destroyed) throw new Error('Database is destroyed')
331
- if (!this.initialized) await this.init()
332
- this.shouldSave && await this.save().catch(console.error)
333
- const matchingLines = await this.indexManager.query(criteria, options)
334
- if (!matchingLines || !matchingLines.size) {
335
- return 0
336
- }
337
- const ranges = this.getRanges([...matchingLines])
338
- const validMatchingLines = new Set(ranges.map(r => r.index))
339
- await this.fileHandler.replaceLines(ranges, [])
340
- const offsets = []
341
- let byteOffset = 0, k = 0
342
- this.offsets.forEach((n, i) => {
343
- if (validMatchingLines.has(i)) {
344
- const r = ranges[k]
345
- byteOffset -= (r.end - r.start)
346
- k++
347
- } else {
348
- offsets.push(n + byteOffset)
349
- }
350
- })
351
- this.offsets = offsets
352
- this.indexOffset += byteOffset
353
- this.indexManager.remove([...validMatchingLines])
354
- this.shouldSave = true
355
- return ranges.length
356
- }
357
-
358
- async destroy() {
359
- this.shouldSave && await this.save().catch(console.error)
360
- this.destroyed = true
361
- this.indexOffset = 0
362
- this.indexManager.index = {}
363
- this.writeBuffer.length = 0
364
- this.initialized = false
365
- this.fileHandler.destroy()
366
- }
367
-
368
- get length() {
369
- return this?.offsets?.length || 0
370
- }
371
-
372
- get index() {
373
- return this.indexManager.index
374
- }
375
-
376
- }
@@ -1,202 +0,0 @@
1
- import fs from 'fs'
2
- import pLimit from 'p-limit'
3
-
4
- export default class FileHandler {
5
- constructor(file) {
6
- this.file = file
7
- }
8
-
9
- async truncate(offset) {
10
- try {
11
- await fs.promises.access(this.file, fs.constants.F_OK)
12
- await fs.promises.truncate(this.file, offset)
13
- } catch (err) {
14
- await fs.promises.writeFile(this.file, '')
15
- }
16
- }
17
-
18
- async readRange(start, end) {
19
- let fd = await fs.promises.open(this.file, 'r')
20
- const length = end - start
21
- let buffer = Buffer.alloc(length)
22
- const { bytesRead } = await fd.read(buffer, 0, length, start).catch(console.error)
23
- await fd.close()
24
- if(buffer.length > bytesRead) return buffer.subarray(0, bytesRead)
25
- return buffer
26
- }
27
-
28
- async readRanges(ranges, mapper) {
29
- const lines = {}, limit = pLimit(4)
30
- const fd = await fs.promises.open(this.file, 'r')
31
- const groupedRanges = await this.groupedRanges(ranges)
32
- try {
33
- await Promise.allSettled(groupedRanges.map(async (groupedRange) => {
34
- await limit(async () => {
35
- for await (const row of this.readGroupedRange(groupedRange, fd)) {
36
- lines[row.start] = mapper ? (await mapper(row.line, groupedRange)) : row.line
37
- }
38
- })
39
- }))
40
- } catch (e) {
41
- console.error('Error reading ranges:', e)
42
- } finally {
43
- await fd.close()
44
- }
45
- return lines
46
- }
47
-
48
- async groupedRanges(ranges) { // expects ordered ranges from Database.getRanges()
49
- const readSize = 512 * 1024 // 512KB
50
- const groupedRanges = []
51
- let currentGroup = []
52
- let currentSize = 0
53
-
54
- // each range is a {start: number, end: number} object
55
- for(const range of ranges) {
56
- const rangeSize = range.end - range.start
57
-
58
- if(currentGroup.length > 0) {
59
- const lastRange = currentGroup[currentGroup.length - 1]
60
- if(lastRange.end !== range.start || currentSize + rangeSize > readSize) {
61
- groupedRanges.push(currentGroup)
62
- currentGroup = []
63
- currentSize = 0
64
- }
65
- }
66
-
67
- currentGroup.push(range)
68
- currentSize += rangeSize
69
- }
70
-
71
- if(currentGroup.length > 0) {
72
- groupedRanges.push(currentGroup)
73
- }
74
-
75
- return groupedRanges
76
- }
77
-
78
- async *readGroupedRange(groupedRange, fd) {
79
- const options = {start: groupedRange[0].start, end: groupedRange[groupedRange.length - 1].end}
80
-
81
- let i = 0, buffer = Buffer.alloc(options.end - options.start)
82
- const results = {}, { bytesRead } = await fd.read(buffer, 0, options.end - options.start, options.start)
83
- if(buffer.length > bytesRead) buffer = buffer.subarray(0, bytesRead)
84
-
85
- for (const range of groupedRange) {
86
- const startOffset = range.start - options.start;
87
- let endOffset = range.end - options.start;
88
- if (endOffset > buffer.length) {
89
- endOffset = buffer.length;
90
- }
91
- if (startOffset >= buffer.length) {
92
- continue;
93
- }
94
- const line = buffer.subarray(startOffset, endOffset);
95
- if (line.length === 0) continue;
96
- yield { line, start: range.start };
97
- }
98
-
99
-
100
- return results
101
- }
102
-
103
- async *walk(ranges) {
104
- const fd = await fs.promises.open(this.file, 'r')
105
- try {
106
- const groupedRanges = await this.groupedRanges(ranges)
107
- for(const groupedRange of groupedRanges) {
108
- for await (const row of this.readGroupedRange(groupedRange, fd)) {
109
- yield row
110
- }
111
- }
112
- } finally {
113
- await fd.close()
114
- }
115
- }
116
-
117
- async replaceLines(ranges, lines) {
118
- const tmpFile = this.file + '.tmp';
119
- const writer = await fs.promises.open(tmpFile, 'w+');
120
- const reader = await fs.promises.open(this.file, 'r');
121
- try {
122
- let position = 0;
123
- let lineIndex = 0;
124
-
125
- for (const range of ranges) {
126
- if (position < range.start) {
127
- const buffer = await this.readRange(position, range.start);
128
- await writer.write(buffer);
129
- }
130
- if (lineIndex < lines.length && lines[lineIndex]) {
131
- await writer.write(lines[lineIndex]);
132
- }
133
- position = range.end;
134
- lineIndex++;
135
- }
136
-
137
- const { size } = await reader.stat();
138
- if (position < size) {
139
- const buffer = await this.readRange(position, size);
140
- await writer.write(buffer);
141
- }
142
-
143
- await reader.close();
144
- await writer.close();
145
- await fs.promises.rename(tmpFile, this.file);
146
- } catch (e) {
147
- console.error('Erro ao substituir linhas:', e);
148
- throw e;
149
- } finally {
150
- await reader.close().catch(() => { });
151
- await writer.close().catch(() => { });
152
- await fs.promises.unlink(tmpFile).catch(() => { });
153
- }
154
- }
155
-
156
- async writeData(data, immediate, fd) {
157
- await fd.write(data)
158
- }
159
-
160
- writeDataSync(data) {
161
- fs.writeFileSync(this.file, data, { flag: 'a' })
162
- }
163
-
164
- async readLastLine() {
165
- const reader = await fs.promises.open(this.file, 'r')
166
- try {
167
- const { size } = await reader.stat()
168
- if (size < 1) throw 'empty file'
169
- this.size = size
170
- const bufferSize = 16384
171
- let buffer, isFirstRead = true, lastReadSize, readPosition = Math.max(size - bufferSize, 0)
172
- while (readPosition >= 0) {
173
- const readSize = Math.min(bufferSize, size - readPosition)
174
- if (readSize !== lastReadSize) {
175
- lastReadSize = readSize
176
- buffer = Buffer.alloc(readSize)
177
- }
178
- const { bytesRead } = await reader.read(buffer, 0, isFirstRead ? (readSize - 1) : readSize, readPosition)
179
- if (isFirstRead) isFirstRead = false
180
- if (bytesRead === 0) break
181
- const newlineIndex = buffer.lastIndexOf(10)
182
- const start = readPosition + newlineIndex + 1
183
- if (newlineIndex !== -1) {
184
- const lastLine = Buffer.alloc(size - start)
185
- await reader.read(lastLine, 0, size - start, start)
186
- if (!lastLine || !lastLine.length) {
187
- throw 'no metadata or empty file'
188
- }
189
- return lastLine
190
- } else {
191
- readPosition -= bufferSize
192
- }
193
- }
194
- } catch (e) {
195
- String(e).includes('empty file') || console.error('Error reading last line:', e)
196
- } finally {
197
- reader.close()
198
- }
199
- }
200
-
201
- async destroy() {}
202
- }