jexidb 1.0.2
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/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/babel.config.json +5 -0
- package/dist/Database.cjs +983 -0
- package/package.json +58 -0
- package/src/Database.mjs +365 -0
- package/src/FileHandler.mjs +133 -0
- package/src/IndexManager.mjs +186 -0
- package/src/serializers/Advanced.mjs +120 -0
- package/src/serializers/Simple.mjs +21 -0
- package/test/test-json-compressed.jdb +0 -0
- package/test/test-json.jdb +12 -0
- package/test/test-v8-compressed.jdb +0 -0
- package/test/test-v8.jdb +0 -0
- package/test/test.mjs +152 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jexidb",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "JexiDB is a pure JS NPM library for managing data on disk using JSONL efficiently, without the need for a server.",
|
|
5
|
+
"main": "./dist/Database.cjs",
|
|
6
|
+
"module": "./src/Database.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/Database.cjs",
|
|
10
|
+
"import": "./src/Database.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node test/test.mjs && exit 1",
|
|
15
|
+
"build": "npx babel src/Database.mjs --plugins @babel/plugin-transform-async-generator-functions --out-file-extension .cjs --out-dir dist"
|
|
16
|
+
},
|
|
17
|
+
"author": "EdenwareApps",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@babel/cli": "^7.25.6",
|
|
21
|
+
"@babel/core": "^7.25.2",
|
|
22
|
+
"@babel/plugin-transform-async-generator-functions": "^7.25.4",
|
|
23
|
+
"@babel/preset-env": "^7.25.4"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"p-limit": "^6.1.0"
|
|
27
|
+
},
|
|
28
|
+
"directories": {
|
|
29
|
+
"test": "test"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/EdenwareApps/jexidb.git"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"couchdb",
|
|
37
|
+
"database",
|
|
38
|
+
"nosql",
|
|
39
|
+
"pouchdb",
|
|
40
|
+
"local-storage",
|
|
41
|
+
"db",
|
|
42
|
+
"persistent-storage",
|
|
43
|
+
"dexiejs",
|
|
44
|
+
"embedded-database",
|
|
45
|
+
"data-management",
|
|
46
|
+
"nedb",
|
|
47
|
+
"lowdb",
|
|
48
|
+
"dexie",
|
|
49
|
+
"offline-database",
|
|
50
|
+
"simple-database",
|
|
51
|
+
"fast-database",
|
|
52
|
+
"jexidb"
|
|
53
|
+
],
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/EdenwareApps/jexidb/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/EdenwareApps/jexidb#readme"
|
|
58
|
+
}
|
package/src/Database.mjs
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import FileHandler from './FileHandler.mjs'
|
|
3
|
+
import IndexManager from './IndexManager.mjs'
|
|
4
|
+
import Serializer from './serializers/Simple.mjs'
|
|
5
|
+
import AdvancedSerializer from './serializers/Advanced.mjs'
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
|
|
8
|
+
export class Database extends EventEmitter {
|
|
9
|
+
constructor(filePath, 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.shouldSave = false
|
|
20
|
+
if(this.opts.v8 || this.opts.compress || this.opts.compressIndex) {
|
|
21
|
+
this.serializer = new AdvancedSerializer(this.opts)
|
|
22
|
+
} else {
|
|
23
|
+
this.serializer = new Serializer(this.opts)
|
|
24
|
+
}
|
|
25
|
+
this.fileHandler = new FileHandler(filePath)
|
|
26
|
+
this.indexManager = new IndexManager(this.opts)
|
|
27
|
+
this.indexOffset = 0
|
|
28
|
+
this.writeBuffer = []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
use(plugin) {
|
|
32
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
33
|
+
plugin(this)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async init() {
|
|
37
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
38
|
+
if(this.initialized) return
|
|
39
|
+
if(this.initlializing) return await new Promise(resolve => this.once('init', resolve))
|
|
40
|
+
this.initializing = true
|
|
41
|
+
try {
|
|
42
|
+
if(this.opts.clear) {
|
|
43
|
+
await this.fileHandler.truncate(0).catch(console.error)
|
|
44
|
+
throw new Error('Cleared, empty file')
|
|
45
|
+
}
|
|
46
|
+
const lastLine = await this.fileHandler.readLastLine()
|
|
47
|
+
if(!lastLine || !lastLine.length) {
|
|
48
|
+
throw new Error('File does not exists or is a empty file')
|
|
49
|
+
}
|
|
50
|
+
const offsets = await this.serializer.deserialize(lastLine, {compress: this.opts.compressIndex})
|
|
51
|
+
if(!Array.isArray(offsets)) {
|
|
52
|
+
throw new Error('File to parse offsets, expected an array')
|
|
53
|
+
}
|
|
54
|
+
this.indexOffset = offsets[offsets.length - 2]
|
|
55
|
+
this.offsets = offsets
|
|
56
|
+
const ptr = this.locate(offsets.length - 2)
|
|
57
|
+
this.offsets = this.offsets.slice(0, -2)
|
|
58
|
+
this.shouldTruncate = true
|
|
59
|
+
let indexLine = await this.fileHandler.readRange(...ptr)
|
|
60
|
+
const index = await this.serializer.deserialize(indexLine, {compress: this.opts.compressIndex})
|
|
61
|
+
index && this.indexManager.load(index)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if(!this.offsets) {
|
|
64
|
+
this.offsets = []
|
|
65
|
+
}
|
|
66
|
+
this.indexOffset = 0
|
|
67
|
+
if(!String(e).includes('empty file')) {
|
|
68
|
+
console.error('Error loading database:', e)
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
this.initializing = false
|
|
72
|
+
this.initialized = true
|
|
73
|
+
this.emit('init')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async save() {
|
|
78
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
79
|
+
if(this.saving) return new Promise(resolve => this.once('save', resolve))
|
|
80
|
+
this.saving = true
|
|
81
|
+
await this.flush()
|
|
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})
|
|
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
|
+
const offsetsString = await this.serializer.serialize(offsets, {compress: this.opts.compressIndex, linebreak: false})
|
|
99
|
+
if (this.shouldTruncate) {
|
|
100
|
+
await this.fileHandler.truncate(this.indexOffset)
|
|
101
|
+
this.shouldTruncate = false
|
|
102
|
+
}
|
|
103
|
+
this.writeBuffer.push(indexString)
|
|
104
|
+
this.writeBuffer.push(offsetsString)
|
|
105
|
+
await this.flush() // write the index and offsets
|
|
106
|
+
this.shouldTruncate = true
|
|
107
|
+
this.shouldSave = false
|
|
108
|
+
this.saving = false
|
|
109
|
+
this.emit('save')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async ready() {
|
|
113
|
+
if (!this.initialized) {
|
|
114
|
+
await new Promise(resolve => this.once('init', resolve))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
locate(n) {
|
|
119
|
+
if (this.offsets[n] === undefined) {
|
|
120
|
+
if(this.offsets[n - 1]) {
|
|
121
|
+
return [this.indexOffset, Number.MAX_SAFE_INTEGER]
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
let end = this.offsets[n + 1] || this.indexOffset || Number.MAX_SAFE_INTEGER
|
|
126
|
+
return [this.offsets[n], end]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getRanges(map) {
|
|
130
|
+
return (map || Array.from(this.offsets.keys())).map(n => {
|
|
131
|
+
const ret = this.locate(n)
|
|
132
|
+
if(ret !== undefined) return {start: ret[0], end: ret[1], index: n}
|
|
133
|
+
}).filter(n => n !== undefined)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async readLines(map, ranges) {
|
|
137
|
+
if(!ranges) ranges = this.getRanges(map)
|
|
138
|
+
const results = await this.fileHandler.readRanges(ranges, this.serializer.deserialize.bind(this.serializer))
|
|
139
|
+
let i = 0
|
|
140
|
+
for(const start in results) {
|
|
141
|
+
if(!results[start] || results[start]._ !== undefined) continue
|
|
142
|
+
while(this.offsets[i] != start && i < map.length) i++ // weak comparison as 'start' is a string
|
|
143
|
+
results[start]._ = map[i++]
|
|
144
|
+
}
|
|
145
|
+
return Object.values(results).filter(r => r !== undefined)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async insert(data) {
|
|
149
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
150
|
+
const line = await this.serializer.serialize(data, {compress: this.opts.compress}) // using Buffer for offsets accuracy
|
|
151
|
+
if (this.shouldTruncate) {
|
|
152
|
+
this.writeBuffer.push(this.indexOffset)
|
|
153
|
+
this.shouldTruncate = false
|
|
154
|
+
}
|
|
155
|
+
const position = this.offsets.length
|
|
156
|
+
this.offsets.push(this.indexOffset)
|
|
157
|
+
this.indexOffset += line.length
|
|
158
|
+
this.indexManager.add(data, position)
|
|
159
|
+
this.emit('insert', data, position)
|
|
160
|
+
this.writeBuffer.push(line)
|
|
161
|
+
if(!this.flushing && this.currentWriteBufferSize() > this.opts.maxMemoryUsage) {
|
|
162
|
+
await this.flush()
|
|
163
|
+
}
|
|
164
|
+
this.shouldSave = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
currentWriteBufferSize(){
|
|
168
|
+
const lengths = this.writeBuffer.filter(b => Buffer.isBuffer(b)).map(b => b.length)
|
|
169
|
+
return lengths.reduce((a, b) => a + b, 0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
flush() {
|
|
173
|
+
if(this.flushing) return this.flushing
|
|
174
|
+
return 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.flushing = this._flush().catch(e => err = e).finally(() => {
|
|
179
|
+
this.flushing = false
|
|
180
|
+
err ? reject(err) : resolve()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async _flush() {
|
|
186
|
+
let fd = await fs.promises.open(this.fileHandler.filePath, 'a')
|
|
187
|
+
try {
|
|
188
|
+
while(this.writeBuffer.length) {
|
|
189
|
+
let data
|
|
190
|
+
const pos = this.writeBuffer.findIndex(b => typeof b === 'number')
|
|
191
|
+
if(pos === 0) {
|
|
192
|
+
await fd.close()
|
|
193
|
+
await this.fileHandler.truncate(this.writeBuffer.shift())
|
|
194
|
+
fd = await fs.promises.open(this.fileHandler.filePath, 'a')
|
|
195
|
+
continue
|
|
196
|
+
} else if(pos === -1) {
|
|
197
|
+
data = Buffer.concat(this.writeBuffer)
|
|
198
|
+
this.writeBuffer.length = 0
|
|
199
|
+
} else {
|
|
200
|
+
data = Buffer.concat(this.writeBuffer.slice(0, pos))
|
|
201
|
+
this.writeBuffer.splice(0, pos)
|
|
202
|
+
}
|
|
203
|
+
await fd.write(data)
|
|
204
|
+
}
|
|
205
|
+
this.shouldSave = true
|
|
206
|
+
} catch(err) {
|
|
207
|
+
console.error('Error flushing:', err)
|
|
208
|
+
} finally {
|
|
209
|
+
await fd.close()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async *walk(map, options={}) {
|
|
214
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
215
|
+
this.shouldSave && await this.save().catch(console.error)
|
|
216
|
+
if(this.indexOffset === 0) return
|
|
217
|
+
if(!Array.isArray(map)) {
|
|
218
|
+
if(map && typeof map === 'object') {
|
|
219
|
+
map = this.indexManager.query(map, options.matchAny)
|
|
220
|
+
} else {
|
|
221
|
+
map = [...Array(this.offsets.length).keys()]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const ranges = this.getRanges(map)
|
|
225
|
+
const partitionedRanges = [], currentPartition = 0
|
|
226
|
+
for (const line in ranges) {
|
|
227
|
+
if (partitionedRanges[currentPartition] === undefined) {
|
|
228
|
+
partitionedRanges[currentPartition] = []
|
|
229
|
+
}
|
|
230
|
+
partitionedRanges[currentPartition].push(ranges[line])
|
|
231
|
+
if (partitionedRanges[currentPartition].length >= this.opts.maxMemoryUsage) {
|
|
232
|
+
currentPartition++
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let m = 0
|
|
236
|
+
for (const ranges of partitionedRanges) {
|
|
237
|
+
const lines = await this.fileHandler.readRanges(ranges)
|
|
238
|
+
for (const line in lines) {
|
|
239
|
+
let err
|
|
240
|
+
const entry = await this.serializer.deserialize(lines[line]).catch(e => console.error(err = e))
|
|
241
|
+
if (err) continue
|
|
242
|
+
if (entry._ === undefined) {
|
|
243
|
+
while(this.offsets[m] != line && m < map.length) m++ // weak comparison as 'start' is a string
|
|
244
|
+
entry._ = m++
|
|
245
|
+
}
|
|
246
|
+
yield entry
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async query(criteria, options={}) {
|
|
252
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
253
|
+
this.shouldSave && await this.save().catch(console.error)
|
|
254
|
+
if(Array.isArray(criteria)) {
|
|
255
|
+
let results = await this.readLines(criteria)
|
|
256
|
+
if (options.orderBy) {
|
|
257
|
+
const [field, direction = 'asc'] = options.orderBy.split(' ')
|
|
258
|
+
results.sort((a, b) => {
|
|
259
|
+
if (a[field] > b[field]) return direction === 'asc' ? 1 : -1
|
|
260
|
+
if (a[field] < b[field]) return direction === 'asc' ? -1 : 1
|
|
261
|
+
return 0;
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
if (options.limit) {
|
|
265
|
+
results = results.slice(0, options.limit);
|
|
266
|
+
}
|
|
267
|
+
return results
|
|
268
|
+
} else {
|
|
269
|
+
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
270
|
+
if (!matchingLines || !matchingLines.size) {
|
|
271
|
+
return []
|
|
272
|
+
}
|
|
273
|
+
return await this.query([...matchingLines], options)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async update(criteria, data, options={}) {
|
|
278
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
279
|
+
this.shouldSave && await this.save().catch(console.error)
|
|
280
|
+
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
281
|
+
if (!matchingLines || !matchingLines.size) {
|
|
282
|
+
return []
|
|
283
|
+
}
|
|
284
|
+
const ranges = this.getRanges([...matchingLines])
|
|
285
|
+
const validMatchingLines = new Set(ranges.map(r => r.index))
|
|
286
|
+
if (!validMatchingLines.size) {
|
|
287
|
+
return []
|
|
288
|
+
}
|
|
289
|
+
const entries = await this.readLines([...validMatchingLines], ranges)
|
|
290
|
+
const lines = []
|
|
291
|
+
for(const entry of entries) {
|
|
292
|
+
let err
|
|
293
|
+
const updated = Object.assign(entry, data)
|
|
294
|
+
const ret = await this.serializer.serialize(updated).catch(e => err = e)
|
|
295
|
+
err || lines.push(ret)
|
|
296
|
+
}
|
|
297
|
+
const offsets = []
|
|
298
|
+
let byteOffset = 0, k = 0
|
|
299
|
+
this.offsets.forEach((n, i) => {
|
|
300
|
+
const prevByteOffset = byteOffset
|
|
301
|
+
if (validMatchingLines.has(i) && ranges[k]) {
|
|
302
|
+
const r = ranges[k]
|
|
303
|
+
byteOffset += lines[k].length - (r.end - r.start)
|
|
304
|
+
k++
|
|
305
|
+
}
|
|
306
|
+
offsets.push(n + prevByteOffset)
|
|
307
|
+
})
|
|
308
|
+
this.offsets = offsets
|
|
309
|
+
this.indexOffset += byteOffset
|
|
310
|
+
await this.fileHandler.replaceLines(ranges, lines);
|
|
311
|
+
[...validMatchingLines].forEach((lineNumber, i) => {
|
|
312
|
+
this.indexManager.dryRemove(lineNumber)
|
|
313
|
+
this.indexManager.add(entries[i], lineNumber)
|
|
314
|
+
})
|
|
315
|
+
this.shouldSave = true
|
|
316
|
+
return entries
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async delete(criteria, options={}) {
|
|
320
|
+
if(this.destroyed) throw new Error('Database is destroyed')
|
|
321
|
+
this.shouldSave && await this.save().catch(console.error)
|
|
322
|
+
const matchingLines = await this.indexManager.query(criteria, options.matchAny)
|
|
323
|
+
if (!matchingLines || !matchingLines.size) {
|
|
324
|
+
return 0
|
|
325
|
+
}
|
|
326
|
+
const ranges = this.getRanges([...matchingLines])
|
|
327
|
+
const validMatchingLines = new Set(ranges.map(r => r.index))
|
|
328
|
+
await this.fileHandler.replaceLines(ranges, [])
|
|
329
|
+
const offsets = []
|
|
330
|
+
let byteOffset = 0, k = 0
|
|
331
|
+
this.offsets.forEach((n, i) => {
|
|
332
|
+
if (validMatchingLines.has(i)) {
|
|
333
|
+
const r = ranges[k]
|
|
334
|
+
byteOffset -= (r.end - r.start)
|
|
335
|
+
k++
|
|
336
|
+
} else {
|
|
337
|
+
offsets.push(n + byteOffset)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
this.offsets = offsets
|
|
341
|
+
this.indexOffset += byteOffset
|
|
342
|
+
this.indexManager.remove([...validMatchingLines])
|
|
343
|
+
this.shouldSave = true
|
|
344
|
+
return ranges.length
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async destroy() {
|
|
348
|
+
this.shouldSave && await this.save().catch(console.error)
|
|
349
|
+
this.destroyed = true
|
|
350
|
+
this.indexOffset = 0
|
|
351
|
+
this.indexManager.index = {}
|
|
352
|
+
this.writeBuffer.length = 0
|
|
353
|
+
this.initialized = false
|
|
354
|
+
this.fileHandler.destroy()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
get length() {
|
|
358
|
+
return this.offsets.length
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
get index() {
|
|
362
|
+
return this.indexManager.index
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import pLimit from 'p-limit'
|
|
3
|
+
|
|
4
|
+
export default class FileHandler {
|
|
5
|
+
constructor(filePath) {
|
|
6
|
+
this.filePath = filePath
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async truncate(offset) {
|
|
10
|
+
try {
|
|
11
|
+
await fs.promises.access(this.filePath, fs.constants.F_OK)
|
|
12
|
+
await fs.promises.truncate(this.filePath, offset)
|
|
13
|
+
} catch (err) {
|
|
14
|
+
await fs.promises.writeFile(this.filePath, '')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async readRange(start, end) {
|
|
19
|
+
let fd = await fs.promises.open(this.filePath, '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.filePath, 'r')
|
|
31
|
+
try {
|
|
32
|
+
const tasks = ranges.map(r => {
|
|
33
|
+
return async () => {
|
|
34
|
+
let err
|
|
35
|
+
const length = r.end - r.start
|
|
36
|
+
let buffer = Buffer.alloc(length)
|
|
37
|
+
const { bytesRead } = await fd.read(buffer, 0, length, r.start).catch(e => err = e)
|
|
38
|
+
if (buffer.length > bytesRead) buffer = buffer.subarray(0, bytesRead)
|
|
39
|
+
lines[r.start] = mapper ? (await mapper(buffer, r)) : buffer
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
await Promise.allSettled(tasks.map(limit))
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('Error reading ranges:', e)
|
|
45
|
+
} finally {
|
|
46
|
+
await fd.close()
|
|
47
|
+
}
|
|
48
|
+
return lines
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async replaceLines(ranges, lines) {
|
|
52
|
+
let closed
|
|
53
|
+
const tmpFile = this.filePath + '.tmp'
|
|
54
|
+
const writer = await fs.promises.open(tmpFile, 'w+')
|
|
55
|
+
const reader = await fs.promises.open(this.filePath, 'r')
|
|
56
|
+
try {
|
|
57
|
+
let i = 0, start = 0
|
|
58
|
+
for (const r of ranges) {
|
|
59
|
+
const length = r.start - start
|
|
60
|
+
const buffer = Buffer.alloc(length)
|
|
61
|
+
await reader.read(buffer, 0, length, start)
|
|
62
|
+
start = r.end
|
|
63
|
+
buffer.length && await writer.write(buffer)
|
|
64
|
+
if (lines[i]) {
|
|
65
|
+
await writer.write(lines[i])
|
|
66
|
+
}
|
|
67
|
+
i++
|
|
68
|
+
}
|
|
69
|
+
const size = (await reader.stat()).size
|
|
70
|
+
const length = size - start
|
|
71
|
+
const buffer = Buffer.alloc(length)
|
|
72
|
+
await reader.read(buffer, 0, length, start)
|
|
73
|
+
await writer.write(buffer)
|
|
74
|
+
await reader.close()
|
|
75
|
+
await writer.close()
|
|
76
|
+
closed = true
|
|
77
|
+
await fs.promises.copyFile(tmpFile, this.filePath)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error('Error replacing lines:', e)
|
|
80
|
+
} finally {
|
|
81
|
+
if(!closed) {
|
|
82
|
+
await reader.close()
|
|
83
|
+
await writer.close()
|
|
84
|
+
}
|
|
85
|
+
await fs.promises.unlink(tmpFile).catch(() => {})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async writeData(data, immediate, fd) {
|
|
89
|
+
await fd.write(data)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writeDataSync(data) {
|
|
93
|
+
fs.writeFileSync(this.filePath, data, { flag: 'a' })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async readLastLine() {
|
|
97
|
+
const reader = await fs.promises.open(this.filePath, 'r')
|
|
98
|
+
try {
|
|
99
|
+
const { size } = await reader.stat()
|
|
100
|
+
if (size < 1) throw 'empty file'
|
|
101
|
+
this.size = size
|
|
102
|
+
const bufferSize = 16384
|
|
103
|
+
let buffer, lastReadSize, readPosition = Math.max(size - bufferSize, 0)
|
|
104
|
+
while (readPosition >= 0) {
|
|
105
|
+
const readSize = Math.min(bufferSize, size - readPosition)
|
|
106
|
+
if (readSize !== lastReadSize) {
|
|
107
|
+
lastReadSize = readSize
|
|
108
|
+
buffer = Buffer.alloc(readSize)
|
|
109
|
+
}
|
|
110
|
+
const { bytesRead } = await reader.read(buffer, 0, readSize, readPosition)
|
|
111
|
+
if (bytesRead === 0) break
|
|
112
|
+
const newlineIndex = buffer.lastIndexOf(10, size - 4) // 0x0A is the ASCII code for '\n'
|
|
113
|
+
if (newlineIndex !== -1) {
|
|
114
|
+
const start = readPosition + newlineIndex + 1
|
|
115
|
+
const lastLine = Buffer.alloc(size - start)
|
|
116
|
+
await reader.read(lastLine, 0, size - start, start)
|
|
117
|
+
if (!lastLine || !lastLine.length) {
|
|
118
|
+
throw 'no metadata or empty file'
|
|
119
|
+
}
|
|
120
|
+
return lastLine
|
|
121
|
+
} else {
|
|
122
|
+
readPosition -= bufferSize
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
String(e).includes('empty file') || console.error('Error reading last line:', e)
|
|
127
|
+
} finally {
|
|
128
|
+
reader.close()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async destroy() {}
|
|
133
|
+
}
|