presidium-db 0.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/DiskHashTable.js +548 -0
- package/DiskSortedHashTable.js +725 -0
- package/LICENSE +9 -0
- package/README.md +4 -0
- package/_internal/getPhysicalBlockSize.js +14 -0
- package/_internal/listDiskDevices.js +13 -0
- package/_internal/preallocate.js +12 -0
- package/package.json +41 -0
package/DiskHashTable.js
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const DATA_SLICE_SIZE = 512 * 1024
|
|
4
|
+
|
|
5
|
+
const ENCODING = 'utf8'
|
|
6
|
+
|
|
7
|
+
const EMPTY = 0
|
|
8
|
+
|
|
9
|
+
const OCCUPIED = 1
|
|
10
|
+
|
|
11
|
+
const REMOVED = 2
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @name DiskHashTable
|
|
15
|
+
*
|
|
16
|
+
* @docs
|
|
17
|
+
* ```coffeescript [specscript]
|
|
18
|
+
* new DiskHashTable(options {
|
|
19
|
+
* initialLength: number,
|
|
20
|
+
* storageFilepath: string,
|
|
21
|
+
* headerFilepath: string,
|
|
22
|
+
* resizeRatio: number,
|
|
23
|
+
* }) -> ht DiskHashTable
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Presidium DiskHashTable class. Creates a hash table that stores all data on disk.
|
|
27
|
+
*
|
|
28
|
+
* Arguments:
|
|
29
|
+
* * `options`
|
|
30
|
+
* * `initialLength` - `number` - the initial length of the disk hash table. Defaults to 1024.
|
|
31
|
+
* * `storageFilepath` - `string` - the path to the file used to store the disk hash table data.
|
|
32
|
+
* * `headerFilepath` - `string` - the path to the file used to store header information about the disk hash table.
|
|
33
|
+
* * `resizeRatio` - `number` - the ratio of number of items to table length at which to resize the table. Minimum value 0 (no resize), maximum value 1. Defaults to 0.
|
|
34
|
+
*
|
|
35
|
+
* Return:
|
|
36
|
+
* * `ht` - [`DiskHashTable`](/docs/DiskHashTable) - a `DiskHashTable` instance.
|
|
37
|
+
*
|
|
38
|
+
* ```javascript
|
|
39
|
+
* const ht = new DiskHashTable({
|
|
40
|
+
* initialLength: 1024,
|
|
41
|
+
* filepath: '/path/to/data-file',
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
class DiskHashTable {
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.initialLength = options.initialLength ?? 1024
|
|
48
|
+
this._length = null
|
|
49
|
+
this._count = null
|
|
50
|
+
this.storageFilepath = options.storageFilepath
|
|
51
|
+
this.headerFilepath = options.headerFilepath
|
|
52
|
+
this.storageFd = null
|
|
53
|
+
this.headerFd = null
|
|
54
|
+
this.resizeRatio = options.resizeRatio ?? 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// _initializeHeader() -> headerReadBuffer Promise<Buffer>
|
|
58
|
+
async _initializeHeader() {
|
|
59
|
+
const headerReadBuffer = Buffer.alloc(8)
|
|
60
|
+
headerReadBuffer.writeUInt32BE(this.initialLength, 0)
|
|
61
|
+
headerReadBuffer.writeUInt32BE(0, 4)
|
|
62
|
+
|
|
63
|
+
await this.headerFd.write(headerReadBuffer, {
|
|
64
|
+
offset: 0,
|
|
65
|
+
position: 0,
|
|
66
|
+
length: headerReadBuffer.length,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return headerReadBuffer
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @name init
|
|
74
|
+
*
|
|
75
|
+
* @docs
|
|
76
|
+
* ```coffeescript [specscript]
|
|
77
|
+
* ht.init() -> Promise<>
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Initializes the disk hash table.
|
|
81
|
+
*
|
|
82
|
+
* Arguments:
|
|
83
|
+
* * (none)
|
|
84
|
+
*
|
|
85
|
+
* Return:
|
|
86
|
+
* * Empty promise.
|
|
87
|
+
*
|
|
88
|
+
* ```javascript
|
|
89
|
+
* await ht.init()
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
async init() {
|
|
93
|
+
for (const filepath of [this.storageFilepath, this.headerFilepath]) {
|
|
94
|
+
const dir = filepath.split('/').slice(0, -1).join('/')
|
|
95
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
96
|
+
|
|
97
|
+
const now = new Date()
|
|
98
|
+
try {
|
|
99
|
+
fs.utimesSync(filepath, now, now)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
fs.closeSync(fs.openSync(filepath, 'a'))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.storageFd = await fs.promises.open(this.storageFilepath, 'r+')
|
|
106
|
+
this.headerFd = await fs.promises.open(this.headerFilepath, 'r+')
|
|
107
|
+
|
|
108
|
+
let headerReadBuffer = await this._readHeader()
|
|
109
|
+
if (headerReadBuffer.every(byte => byte === 0)) {
|
|
110
|
+
headerReadBuffer = await this._initializeHeader()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const length = headerReadBuffer.readUInt32BE(0)
|
|
114
|
+
this._length = length
|
|
115
|
+
|
|
116
|
+
const count = headerReadBuffer.readUInt32BE(4)
|
|
117
|
+
this._count = count
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @name clear
|
|
122
|
+
*
|
|
123
|
+
* @docs
|
|
124
|
+
* ```coffeescript [specscript]
|
|
125
|
+
* clear() -> Promise<>
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* Clears all data from the disk hash table.
|
|
129
|
+
*
|
|
130
|
+
* Arguments:
|
|
131
|
+
* * (none)
|
|
132
|
+
*
|
|
133
|
+
* Return:
|
|
134
|
+
* * Empty promise.
|
|
135
|
+
*
|
|
136
|
+
* ```javascript
|
|
137
|
+
* await ht.clear()
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
async clear() {
|
|
141
|
+
this.close()
|
|
142
|
+
|
|
143
|
+
await fs.promises.rm(this.storageFilepath).catch(() => {})
|
|
144
|
+
await fs.promises.rm(this.headerFilepath).catch(() => {})
|
|
145
|
+
|
|
146
|
+
for (const filepath of [this.storageFilepath, this.headerFilepath]) {
|
|
147
|
+
const dir = filepath.split('/').slice(0, -1).join('/')
|
|
148
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
149
|
+
|
|
150
|
+
const now = new Date()
|
|
151
|
+
try {
|
|
152
|
+
fs.utimesSync(filepath, now, now)
|
|
153
|
+
} catch (error) {
|
|
154
|
+
fs.closeSync(fs.openSync(filepath, 'a'))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.storageFd = await fs.promises.open(this.storageFilepath, 'r+')
|
|
159
|
+
this.headerFd = await fs.promises.open(this.headerFilepath, 'r+')
|
|
160
|
+
|
|
161
|
+
const headerReadBuffer = await this._initializeHeader()
|
|
162
|
+
|
|
163
|
+
const length = headerReadBuffer.readUInt32BE(0)
|
|
164
|
+
this._length = length
|
|
165
|
+
|
|
166
|
+
const count = headerReadBuffer.readUInt32BE(4)
|
|
167
|
+
this._count = count
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @name destroy
|
|
172
|
+
*
|
|
173
|
+
* @docs
|
|
174
|
+
* ```coffeescript [specscript]
|
|
175
|
+
* destroy() -> Promise<>
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* Removes all system resources used by the disk hash table.
|
|
179
|
+
*
|
|
180
|
+
* Arguments:
|
|
181
|
+
* * (none)
|
|
182
|
+
*
|
|
183
|
+
* Return:
|
|
184
|
+
* * Empty promise.
|
|
185
|
+
*
|
|
186
|
+
* ```javascript
|
|
187
|
+
* await ht.destroy()
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
async destroy() {
|
|
191
|
+
await fs.promises.rm(this.storageFilepath).catch(() => {})
|
|
192
|
+
await fs.promises.rm(this.headerFilepath).catch(() => {})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @name close
|
|
197
|
+
*
|
|
198
|
+
* @docs
|
|
199
|
+
* ```coffeescript [specscript]
|
|
200
|
+
* close() -> undefined
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* Closes the underlying file handles used by the disk hash table.
|
|
204
|
+
*
|
|
205
|
+
* Arguments:
|
|
206
|
+
* * (none)
|
|
207
|
+
*
|
|
208
|
+
* Return:
|
|
209
|
+
* * `undefined`
|
|
210
|
+
*
|
|
211
|
+
* ```javascript
|
|
212
|
+
* ht.close()
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
close() {
|
|
216
|
+
this.storageFd.close()
|
|
217
|
+
this.headerFd.close()
|
|
218
|
+
this.storageFd = null
|
|
219
|
+
this.headerFd = null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// _hash1(key string) -> number
|
|
223
|
+
_hash1(key) {
|
|
224
|
+
let hashCode = 0
|
|
225
|
+
const prime = 31
|
|
226
|
+
for (let i = 0; i < key.length; i++) {
|
|
227
|
+
hashCode = (prime * hashCode + key.charCodeAt(i)) % this._length
|
|
228
|
+
}
|
|
229
|
+
return hashCode
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// _hash2(key string) -> number
|
|
233
|
+
_hash2(key) {
|
|
234
|
+
let hash = 0
|
|
235
|
+
for (let i = 0; i < key.length; i++) {
|
|
236
|
+
hash = (hash << 3) - hash + key.charCodeAt(i)
|
|
237
|
+
}
|
|
238
|
+
const prime = 7
|
|
239
|
+
return prime - (Math.abs(hash) % prime)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// header file
|
|
243
|
+
// 32 bits / 4 bytes table length
|
|
244
|
+
// 32 bits / 4 bytes item count
|
|
245
|
+
|
|
246
|
+
// _readHeader() -> headerReadBuffer Promise<Buffer>
|
|
247
|
+
async _readHeader() {
|
|
248
|
+
const headerReadBuffer = Buffer.alloc(8)
|
|
249
|
+
|
|
250
|
+
await this.headerFd.read({
|
|
251
|
+
buffer: headerReadBuffer,
|
|
252
|
+
offset: 0,
|
|
253
|
+
position: 0,
|
|
254
|
+
length: 8,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
return headerReadBuffer
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// _read(index number) -> readBuffer Promise<Buffer>
|
|
261
|
+
async _read(index) {
|
|
262
|
+
const position = index * DATA_SLICE_SIZE
|
|
263
|
+
const readBuffer = Buffer.alloc(DATA_SLICE_SIZE)
|
|
264
|
+
|
|
265
|
+
await this.storageFd.read({
|
|
266
|
+
buffer: readBuffer,
|
|
267
|
+
offset: 0,
|
|
268
|
+
position,
|
|
269
|
+
length: DATA_SLICE_SIZE,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return readBuffer
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// _getKey(index number) -> key Promise<string>
|
|
276
|
+
async _getKey(index) {
|
|
277
|
+
if (index == -1) {
|
|
278
|
+
throw new Error('Negative index')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const readBuffer = await this._read(index)
|
|
282
|
+
const statusMarker = readBuffer.readUInt8(0)
|
|
283
|
+
if (statusMarker === EMPTY) {
|
|
284
|
+
return undefined
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const keyByteLength = readBuffer.readUInt32BE(1)
|
|
288
|
+
const keyBuffer = readBuffer.subarray(9, keyByteLength + 9)
|
|
289
|
+
return keyBuffer.toString(ENCODING)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// _setStatusMarker(index number, marker number) -> Promise<>
|
|
293
|
+
async _setStatusMarker(index, marker) {
|
|
294
|
+
const position = index * DATA_SLICE_SIZE
|
|
295
|
+
const buffer = Buffer.alloc(1)
|
|
296
|
+
buffer.writeUInt8(marker, 0)
|
|
297
|
+
|
|
298
|
+
await this.storageFd.write(buffer, {
|
|
299
|
+
offset: 0,
|
|
300
|
+
position,
|
|
301
|
+
length: buffer.length,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// _resize() -> Promise<>
|
|
306
|
+
async _resize() {
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @name set
|
|
311
|
+
*
|
|
312
|
+
* @docs
|
|
313
|
+
* ```coffeescript [specscript]
|
|
314
|
+
* set(key string, value string) -> Promise<>
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* Sets and stores a value by key in the disk hash table.
|
|
318
|
+
*
|
|
319
|
+
* Arguments:
|
|
320
|
+
* * `key` - `string` - the key to set.
|
|
321
|
+
* * `value` - `string` - the value to set corresponding to the key.
|
|
322
|
+
*
|
|
323
|
+
* Return:
|
|
324
|
+
* * Empty promise.
|
|
325
|
+
*
|
|
326
|
+
* ```javascript
|
|
327
|
+
* await ht.set('my-key', 'my-value')
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
async set(key, value) {
|
|
331
|
+
if (this.resizeRatio > 0 && (this._count / this._length) > this.resizeRatio) {
|
|
332
|
+
this._resize()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let index = this._hash1(key)
|
|
336
|
+
|
|
337
|
+
const startIndex = index
|
|
338
|
+
const stepSize = this._hash2(key)
|
|
339
|
+
|
|
340
|
+
let currentKey = await this._getKey(index)
|
|
341
|
+
while (currentKey) {
|
|
342
|
+
if (key == currentKey) {
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
index = (index + stepSize) % this._length
|
|
347
|
+
if (index == startIndex) {
|
|
348
|
+
throw new Error('Disk hash table is full')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
currentKey = await this._getKey(index)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (currentKey == null) { // insert
|
|
355
|
+
await this._incrementCount()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const position = index * DATA_SLICE_SIZE
|
|
359
|
+
const buffer = Buffer.alloc(DATA_SLICE_SIZE)
|
|
360
|
+
|
|
361
|
+
// 8 bits / 1 byte for status marker: 0 empty / 1 occupied / 2 deleted
|
|
362
|
+
// 32 bits / 4 bytes for key size
|
|
363
|
+
// 32 bits / 4 bytes for value size
|
|
364
|
+
// chunk for key
|
|
365
|
+
// remainder for value
|
|
366
|
+
const statusMarker = 1
|
|
367
|
+
const keyByteLength = Buffer.byteLength(key, ENCODING)
|
|
368
|
+
const valueByteLength = Buffer.byteLength(value, ENCODING)
|
|
369
|
+
buffer.writeUInt8(statusMarker, 0)
|
|
370
|
+
buffer.writeUint32BE(keyByteLength, 1)
|
|
371
|
+
buffer.writeUint32BE(valueByteLength, 5)
|
|
372
|
+
buffer.write(key, 9, keyByteLength, ENCODING)
|
|
373
|
+
buffer.write(value, keyByteLength + 9, valueByteLength, ENCODING)
|
|
374
|
+
|
|
375
|
+
await this.storageFd.write(buffer, {
|
|
376
|
+
offset: 0,
|
|
377
|
+
position,
|
|
378
|
+
length: buffer.length,
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @name get
|
|
384
|
+
*
|
|
385
|
+
* @docs
|
|
386
|
+
* ```coffeescript [specscript]
|
|
387
|
+
* get(key string) -> value Promise<string>
|
|
388
|
+
* ```
|
|
389
|
+
*
|
|
390
|
+
* Gets a value by key from the disk hash table.
|
|
391
|
+
*
|
|
392
|
+
* Arguments:
|
|
393
|
+
* * `key` - `string` - the key corresponding to the value.
|
|
394
|
+
*
|
|
395
|
+
* Return:
|
|
396
|
+
* * `value` - `string` - the value corresponding to the key.
|
|
397
|
+
*
|
|
398
|
+
* ```javascript
|
|
399
|
+
* const value = await ht.get('my-key')
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
async get(key) {
|
|
403
|
+
let index = this._hash1(key)
|
|
404
|
+
const startIndex = index
|
|
405
|
+
const stepSize = this._hash2(key)
|
|
406
|
+
|
|
407
|
+
let currentKey = await this._getKey(index)
|
|
408
|
+
while (currentKey) {
|
|
409
|
+
if (key == currentKey) {
|
|
410
|
+
break
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
index = (index + stepSize) % this._length
|
|
414
|
+
if (index == startIndex) {
|
|
415
|
+
return undefined // entire table searched
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
currentKey = await this._getKey(index)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (currentKey == null) {
|
|
422
|
+
return undefined
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const readBuffer = await this._read(index)
|
|
426
|
+
const statusMarker = readBuffer.readUInt8(0)
|
|
427
|
+
if (statusMarker === OCCUPIED) {
|
|
428
|
+
const keyByteLength = readBuffer.readUInt32BE(1)
|
|
429
|
+
const valueByteLength = readBuffer.readUInt32BE(5)
|
|
430
|
+
const keyBuffer = readBuffer.subarray(9, keyByteLength + 9)
|
|
431
|
+
const valueBuffer = readBuffer.subarray(
|
|
432
|
+
9 + keyByteLength,
|
|
433
|
+
9 + keyByteLength + valueByteLength
|
|
434
|
+
)
|
|
435
|
+
return valueBuffer.toString(ENCODING)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return undefined
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @name delete
|
|
443
|
+
*
|
|
444
|
+
* @docs
|
|
445
|
+
* ```coffeescript [specscript]
|
|
446
|
+
* delete(key string) -> didDelete Promise<boolean>
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* Deletes a key and corresponding value from the disk hash table.
|
|
450
|
+
*
|
|
451
|
+
* Arguments:
|
|
452
|
+
* * `key` - `string` - the key to delete.
|
|
453
|
+
*
|
|
454
|
+
* Return:
|
|
455
|
+
* * `didDelete` - `boolean` - a promise of whether the key and corresponding value was deleted.
|
|
456
|
+
*
|
|
457
|
+
* ```javascript
|
|
458
|
+
* const didDelete = await ht.delete('my-key')
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
async delete(key) {
|
|
462
|
+
let index = this._hash1(key)
|
|
463
|
+
const startIndex = index
|
|
464
|
+
const stepSize = this._hash2(key)
|
|
465
|
+
|
|
466
|
+
let currentKey = await this._getKey(index)
|
|
467
|
+
while (currentKey) {
|
|
468
|
+
if (key == currentKey) {
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
index = (index + stepSize) % this._length
|
|
473
|
+
if (index == startIndex) {
|
|
474
|
+
return false // entire table searched
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
currentKey = await this._getKey(index)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (currentKey == null) {
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const readBuffer = await this._read(index)
|
|
485
|
+
const statusMarker = readBuffer.readUInt8(0)
|
|
486
|
+
|
|
487
|
+
if (statusMarker === OCCUPIED) {
|
|
488
|
+
await this._setStatusMarker(index, REMOVED)
|
|
489
|
+
await this._decrementCount()
|
|
490
|
+
return true
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return false
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// _incrementCount() -> Promise<>
|
|
497
|
+
async _incrementCount() {
|
|
498
|
+
this._count += 1
|
|
499
|
+
|
|
500
|
+
const position = 4
|
|
501
|
+
const buffer = Buffer.alloc(4)
|
|
502
|
+
buffer.writeInt32BE(this._count, 0)
|
|
503
|
+
|
|
504
|
+
await this.headerFd.write(buffer, {
|
|
505
|
+
offset: 0,
|
|
506
|
+
position,
|
|
507
|
+
length: 4,
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// _decrementCount() -> Promise<>
|
|
512
|
+
async _decrementCount() {
|
|
513
|
+
this._count -= 1
|
|
514
|
+
|
|
515
|
+
const position = 4
|
|
516
|
+
const buffer = Buffer.alloc(4)
|
|
517
|
+
buffer.writeInt32BE(this._count, 0)
|
|
518
|
+
|
|
519
|
+
await this.headerFd.write(buffer, {
|
|
520
|
+
offset: 0,
|
|
521
|
+
position,
|
|
522
|
+
length: 4,
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @name count
|
|
528
|
+
*
|
|
529
|
+
* @docs
|
|
530
|
+
* ```coffeescript [specscript]
|
|
531
|
+
* count() -> number
|
|
532
|
+
* ```
|
|
533
|
+
*
|
|
534
|
+
* Returns the number of items (key-value pairs) in the disk hash table.
|
|
535
|
+
*
|
|
536
|
+
* Arguments:
|
|
537
|
+
* * (none)
|
|
538
|
+
*
|
|
539
|
+
* Return:
|
|
540
|
+
* * `number` - the number of items in the disk hash table.
|
|
541
|
+
*/
|
|
542
|
+
count() {
|
|
543
|
+
return this._count
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
module.exports = DiskHashTable
|
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const DATA_SLICE_SIZE = 512 * 1024
|
|
4
|
+
|
|
5
|
+
const ENCODING = 'utf8'
|
|
6
|
+
|
|
7
|
+
const EMPTY = 0
|
|
8
|
+
|
|
9
|
+
const OCCUPIED = 1
|
|
10
|
+
|
|
11
|
+
const REMOVED = 2
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @name DiskSortedHashTable
|
|
15
|
+
*
|
|
16
|
+
* @docs
|
|
17
|
+
* ```coffeescript [specscript]
|
|
18
|
+
* new DiskSortedHashTable(options {
|
|
19
|
+
* initialLength: number,
|
|
20
|
+
* storageFilepath: string,
|
|
21
|
+
* headerFilepath: string,
|
|
22
|
+
* }) -> DiskSortedHashTable
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class DiskSortedHashTable {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.initialLength = options.initialLength ?? 1024
|
|
28
|
+
this._length = null
|
|
29
|
+
this._count = null
|
|
30
|
+
this.storageFilepath = options.storageFilepath
|
|
31
|
+
this.headerFilepath = options.headerFilepath
|
|
32
|
+
this.storageFd = null
|
|
33
|
+
this.headerFd = null
|
|
34
|
+
this.resizeRatio = options.resizeRatio ?? 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// _initializeHeader() -> headerReadBuffer Promise<Buffer>
|
|
38
|
+
async _initializeHeader() {
|
|
39
|
+
const headerReadBuffer = Buffer.alloc(16)
|
|
40
|
+
headerReadBuffer.writeUInt32BE(this.initialLength, 0)
|
|
41
|
+
headerReadBuffer.writeUInt32BE(0, 4)
|
|
42
|
+
headerReadBuffer.writeInt32BE(-1, 8)
|
|
43
|
+
headerReadBuffer.writeInt32BE(-1, 12)
|
|
44
|
+
|
|
45
|
+
await this.headerFd.write(headerReadBuffer, {
|
|
46
|
+
offset: 0,
|
|
47
|
+
position: 0,
|
|
48
|
+
length: headerReadBuffer.length,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return headerReadBuffer
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// init() -> Promise<>
|
|
55
|
+
async init() {
|
|
56
|
+
for (const filepath of [this.storageFilepath, this.headerFilepath]) {
|
|
57
|
+
const dir = filepath.split('/').slice(0, -1).join('/')
|
|
58
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
59
|
+
|
|
60
|
+
const now = new Date()
|
|
61
|
+
try {
|
|
62
|
+
fs.utimesSync(filepath, now, now)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
fs.closeSync(fs.openSync(filepath, 'a'))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.storageFd = await fs.promises.open(this.storageFilepath, 'r+')
|
|
69
|
+
this.headerFd = await fs.promises.open(this.headerFilepath, 'r+')
|
|
70
|
+
|
|
71
|
+
let headerReadBuffer = await this._readHeader()
|
|
72
|
+
if (headerReadBuffer.every(byte => byte === 0)) {
|
|
73
|
+
headerReadBuffer = await this._initializeHeader()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const length = headerReadBuffer.readUInt32BE(0)
|
|
77
|
+
this._length = length
|
|
78
|
+
|
|
79
|
+
const count = headerReadBuffer.readUInt32BE(4)
|
|
80
|
+
this._count = count
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// clear() -> Promise<>
|
|
84
|
+
async clear() {
|
|
85
|
+
this.close()
|
|
86
|
+
|
|
87
|
+
await fs.promises.rm(this.storageFilepath).catch(() => {})
|
|
88
|
+
await fs.promises.rm(this.headerFilepath).catch(() => {})
|
|
89
|
+
|
|
90
|
+
for (const filepath of [this.storageFilepath, this.headerFilepath]) {
|
|
91
|
+
const dir = filepath.split('/').slice(0, -1).join('/')
|
|
92
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
93
|
+
|
|
94
|
+
const now = new Date()
|
|
95
|
+
try {
|
|
96
|
+
fs.utimesSync(filepath, now, now)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
fs.closeSync(fs.openSync(filepath, 'a'))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.storageFd = await fs.promises.open(this.storageFilepath, 'r+')
|
|
103
|
+
this.headerFd = await fs.promises.open(this.headerFilepath, 'r+')
|
|
104
|
+
|
|
105
|
+
const headerReadBuffer = await this._initializeHeader()
|
|
106
|
+
|
|
107
|
+
const length = headerReadBuffer.readUInt32BE(0)
|
|
108
|
+
this._length = length
|
|
109
|
+
|
|
110
|
+
const count = headerReadBuffer.readUInt32BE(4)
|
|
111
|
+
this._count = count
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// destroy() -> Promise<>
|
|
115
|
+
async destroy() {
|
|
116
|
+
await fs.promises.rm(this.storageFilepath).catch(() => {})
|
|
117
|
+
await fs.promises.rm(this.headerFilepath).catch(() => {})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// close() -> ()
|
|
121
|
+
close() {
|
|
122
|
+
this.storageFd.close()
|
|
123
|
+
this.headerFd.close()
|
|
124
|
+
this.storageFd = null
|
|
125
|
+
this.headerFd = null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// _hash1(key string) -> number
|
|
129
|
+
_hash1(key) {
|
|
130
|
+
let hashCode = 0
|
|
131
|
+
const prime = 31
|
|
132
|
+
for (let i = 0; i < key.length; i++) {
|
|
133
|
+
hashCode = (prime * hashCode + key.charCodeAt(i)) % this._length
|
|
134
|
+
}
|
|
135
|
+
return hashCode
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// _hash2(key string) -> number
|
|
139
|
+
_hash2(key) {
|
|
140
|
+
let hash = 0
|
|
141
|
+
for (let i = 0; i < key.length; i++) {
|
|
142
|
+
hash = (hash << 3) - hash + key.charCodeAt(i)
|
|
143
|
+
}
|
|
144
|
+
const prime = 7
|
|
145
|
+
return prime - (Math.abs(hash) % prime)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// header file
|
|
149
|
+
// 32 bits / 4 bytes table length
|
|
150
|
+
// 32 bits / 4 bytes item count
|
|
151
|
+
// 32 bits / 4 bytes first item index
|
|
152
|
+
// 32 bits / 4 bytes last item index
|
|
153
|
+
|
|
154
|
+
// _readHeader() -> headerReadBuffer Promise<Buffer>
|
|
155
|
+
async _readHeader() {
|
|
156
|
+
const headerReadBuffer = Buffer.alloc(16)
|
|
157
|
+
|
|
158
|
+
await this.headerFd.read({
|
|
159
|
+
buffer: headerReadBuffer,
|
|
160
|
+
offset: 0,
|
|
161
|
+
position: 0,
|
|
162
|
+
length: 16,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return headerReadBuffer
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// _read(index number) -> readBuffer Promise<Buffer>
|
|
169
|
+
async _read(index) {
|
|
170
|
+
const position = index * DATA_SLICE_SIZE
|
|
171
|
+
const readBuffer = Buffer.alloc(DATA_SLICE_SIZE)
|
|
172
|
+
|
|
173
|
+
await this.storageFd.read({
|
|
174
|
+
buffer: readBuffer,
|
|
175
|
+
offset: 0,
|
|
176
|
+
position,
|
|
177
|
+
length: DATA_SLICE_SIZE,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return readBuffer
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// _writeFirstIndex(index number) -> Promise<>
|
|
184
|
+
async _writeFirstIndex(index) {
|
|
185
|
+
const position = 8
|
|
186
|
+
const buffer = Buffer.alloc(4)
|
|
187
|
+
buffer.writeInt32BE(index, 0)
|
|
188
|
+
|
|
189
|
+
await this.headerFd.write(buffer, {
|
|
190
|
+
offset: 0,
|
|
191
|
+
position,
|
|
192
|
+
length: buffer.length,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// _writeLastIndex(index number) -> Promise<>
|
|
197
|
+
async _writeLastIndex(index) {
|
|
198
|
+
const position = 12
|
|
199
|
+
const buffer = Buffer.alloc(4)
|
|
200
|
+
buffer.writeInt32BE(index, 0)
|
|
201
|
+
|
|
202
|
+
await this.headerFd.write(buffer, {
|
|
203
|
+
offset: 0,
|
|
204
|
+
position,
|
|
205
|
+
length: buffer.length,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// _getKey(index number) -> key Promise<string>
|
|
210
|
+
async _getKey(index) {
|
|
211
|
+
if (index == -1) {
|
|
212
|
+
throw new Error('Negative index')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const readBuffer = await this._read(index)
|
|
216
|
+
|
|
217
|
+
const statusMarker = readBuffer.readUInt8(0)
|
|
218
|
+
if (statusMarker === EMPTY) {
|
|
219
|
+
return undefined
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const keyByteLength = readBuffer.readUInt32BE(1)
|
|
223
|
+
const keyBuffer = readBuffer.subarray(21, keyByteLength + 21)
|
|
224
|
+
return keyBuffer.toString(ENCODING)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// _setStatusMarker(index number, marker number) -> Promise<>
|
|
228
|
+
async _setStatusMarker(index, marker) {
|
|
229
|
+
const position = index * DATA_SLICE_SIZE
|
|
230
|
+
const buffer = Buffer.alloc(1)
|
|
231
|
+
buffer.writeUInt8(marker, 0)
|
|
232
|
+
|
|
233
|
+
await this.storageFd.write(buffer, {
|
|
234
|
+
offset: 0,
|
|
235
|
+
position,
|
|
236
|
+
length: buffer.length,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// _parseItem(readBuffer Buffer, index number) -> { index: number, readBuffer: Buffer, sortValue: string|number, value: string }
|
|
241
|
+
_parseItem(readBuffer, index) {
|
|
242
|
+
const item = {}
|
|
243
|
+
item.index = index
|
|
244
|
+
item.readBuffer = readBuffer
|
|
245
|
+
|
|
246
|
+
const statusMarker = readBuffer.readUInt8(0)
|
|
247
|
+
item.statusMarker = statusMarker
|
|
248
|
+
|
|
249
|
+
const forwardIndex = readBuffer.readInt32BE(13)
|
|
250
|
+
const reverseIndex = readBuffer.readInt32BE(17)
|
|
251
|
+
item.forwardIndex = forwardIndex
|
|
252
|
+
item.reverseIndex = reverseIndex
|
|
253
|
+
|
|
254
|
+
const keyByteLength = readBuffer.readUInt32BE(1)
|
|
255
|
+
const sortValueByteLength = readBuffer.readInt32BE(5)
|
|
256
|
+
const sortValueBuffer = readBuffer.subarray(
|
|
257
|
+
21 + keyByteLength,
|
|
258
|
+
21 + keyByteLength + sortValueByteLength
|
|
259
|
+
)
|
|
260
|
+
const sortValue = sortValueBuffer.toString(ENCODING)
|
|
261
|
+
item.sortValue = sortValue
|
|
262
|
+
|
|
263
|
+
const valueByteLength = readBuffer.readUInt32BE(9)
|
|
264
|
+
const valueBuffer = readBuffer.subarray(
|
|
265
|
+
21 + keyByteLength + sortValueByteLength,
|
|
266
|
+
21 + keyByteLength + sortValueByteLength + valueByteLength
|
|
267
|
+
)
|
|
268
|
+
const value = valueBuffer.toString(ENCODING)
|
|
269
|
+
item.value = value
|
|
270
|
+
|
|
271
|
+
return item
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// _getForwardStartItem() -> item { index: number, readBuffer: Buffer, sortValue: string|number, value: string }
|
|
275
|
+
async _getForwardStartItem() {
|
|
276
|
+
const headerReadBuffer = await this._readHeader()
|
|
277
|
+
const index = headerReadBuffer.readInt32BE(8)
|
|
278
|
+
if (index == -1) {
|
|
279
|
+
return undefined
|
|
280
|
+
}
|
|
281
|
+
const readBuffer = await this._read(index)
|
|
282
|
+
return this._parseItem(readBuffer, index)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// _getReverseStartItem() -> item { index: number, readBuffer: Buffer, sortValue: string|number, value: string }
|
|
286
|
+
async _getReverseStartItem() {
|
|
287
|
+
const headerReadBuffer = await this._readHeader()
|
|
288
|
+
const index = headerReadBuffer.readInt32BE(12)
|
|
289
|
+
if (index == -1) {
|
|
290
|
+
return undefined
|
|
291
|
+
}
|
|
292
|
+
const readBuffer = await this._read(index)
|
|
293
|
+
return this._parseItem(readBuffer, index)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// _getItem(index number) -> item { index: number, readBuffer: Buffer, sortValue: string|number, value: string }
|
|
297
|
+
async _getItem(index) {
|
|
298
|
+
if (index == -1) {
|
|
299
|
+
return undefined
|
|
300
|
+
}
|
|
301
|
+
const readBuffer = await this._read(index)
|
|
302
|
+
return this._parseItem(readBuffer, index)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// _updateForwardIndex(index number, forwardIndex number) -> Promise<>
|
|
306
|
+
async _updateForwardIndex(index, forwardIndex) {
|
|
307
|
+
const position = (index * DATA_SLICE_SIZE) + 13
|
|
308
|
+
const buffer = Buffer.alloc(4)
|
|
309
|
+
buffer.writeInt32BE(forwardIndex, 0)
|
|
310
|
+
|
|
311
|
+
await this.storageFd.write(buffer, {
|
|
312
|
+
offset: 0,
|
|
313
|
+
position,
|
|
314
|
+
length: buffer.length,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// _updateReverseIndex(index number, reverseIndex number) -> Promise<>
|
|
319
|
+
async _updateReverseIndex(index, reverseIndex) {
|
|
320
|
+
const position = (index * DATA_SLICE_SIZE) + 17
|
|
321
|
+
const buffer = Buffer.alloc(4)
|
|
322
|
+
buffer.writeInt32BE(reverseIndex, 0)
|
|
323
|
+
|
|
324
|
+
await this.storageFd.write(buffer, {
|
|
325
|
+
offset: 0,
|
|
326
|
+
position,
|
|
327
|
+
length: buffer.length,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// _insert(key string, value string, sortValue number|string, index number) -> Promise<>
|
|
332
|
+
async _insert(key, value, sortValue, index) {
|
|
333
|
+
const forwardStartItem = await this._getForwardStartItem()
|
|
334
|
+
let previousForwardItem = null
|
|
335
|
+
let currentForwardItem = forwardStartItem
|
|
336
|
+
while (currentForwardItem) {
|
|
337
|
+
const left = typeof sortValue == 'string' ? currentForwardItem.sortValue : Number(currentForwardItem.sortValue)
|
|
338
|
+
if (sortValue > left) {
|
|
339
|
+
previousForwardItem = currentForwardItem
|
|
340
|
+
currentForwardItem = await this._getItem(previousForwardItem.forwardIndex)
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let reverseIndex = -1
|
|
347
|
+
let forwardIndex = -1
|
|
348
|
+
if (previousForwardItem == null) { // item to insert is first in the list
|
|
349
|
+
await this._writeFirstIndex(index)
|
|
350
|
+
if (forwardStartItem == null) { // item to insert is also last in the list
|
|
351
|
+
await this._writeLastIndex(index)
|
|
352
|
+
} else {
|
|
353
|
+
forwardIndex = forwardStartItem.index
|
|
354
|
+
await this._updateReverseIndex(forwardStartItem.index, index)
|
|
355
|
+
}
|
|
356
|
+
} else if (previousForwardItem.forwardIndex == -1) { // item to insert is the last in the list
|
|
357
|
+
await this._writeLastIndex(index)
|
|
358
|
+
await this._updateForwardIndex(previousForwardItem.index, index)
|
|
359
|
+
reverseIndex = previousForwardItem.index
|
|
360
|
+
} else { // item to insert is ahead of previousForwardItem and there was an item ahead of previousForwardItem
|
|
361
|
+
await this._updateForwardIndex(previousForwardItem.index, index)
|
|
362
|
+
await this._updateReverseIndex(currentForwardItem.index, index)
|
|
363
|
+
forwardIndex = previousForwardItem.forwardIndex
|
|
364
|
+
reverseIndex = previousForwardItem.index
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const position = index * DATA_SLICE_SIZE
|
|
368
|
+
const buffer = Buffer.alloc(DATA_SLICE_SIZE)
|
|
369
|
+
const sortValueString = typeof sortValue == 'string' ? sortValue : sortValue.toString()
|
|
370
|
+
|
|
371
|
+
// 8 bits / 1 byte for status marker: 0 empty / 1 occupied / 2 deleted
|
|
372
|
+
// 32 bits / 4 bytes for key size
|
|
373
|
+
// 32 bits / 4 bytes for sort value size
|
|
374
|
+
// 32 bits / 4 bytes for value size
|
|
375
|
+
// 32 bits / 4 bytes for forward index
|
|
376
|
+
// 32 bits / 4 bytes for reverse index
|
|
377
|
+
// chunk for key
|
|
378
|
+
// chunk for sort value
|
|
379
|
+
// remainder for value
|
|
380
|
+
const statusMarker = 1
|
|
381
|
+
const keyByteLength = Buffer.byteLength(key, ENCODING)
|
|
382
|
+
const sortValueByteLength = Buffer.byteLength(sortValueString, ENCODING)
|
|
383
|
+
const valueByteLength = Buffer.byteLength(value, ENCODING)
|
|
384
|
+
buffer.writeUInt8(statusMarker, 0)
|
|
385
|
+
buffer.writeUint32BE(keyByteLength, 1)
|
|
386
|
+
buffer.writeUint32BE(sortValueByteLength, 5)
|
|
387
|
+
buffer.writeUint32BE(valueByteLength, 9)
|
|
388
|
+
buffer.writeInt32BE(forwardIndex, 13)
|
|
389
|
+
buffer.writeInt32BE(reverseIndex, 17)
|
|
390
|
+
buffer.write(key, 21, keyByteLength, ENCODING)
|
|
391
|
+
buffer.write(sortValueString, 21 + keyByteLength, sortValueByteLength, ENCODING)
|
|
392
|
+
buffer.write(value, 21 + keyByteLength + sortValueByteLength, valueByteLength, ENCODING)
|
|
393
|
+
|
|
394
|
+
await this.storageFd.write(buffer, {
|
|
395
|
+
offset: 0,
|
|
396
|
+
position,
|
|
397
|
+
length: buffer.length,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// _update(key string, value string, sortValue number|string, index number) -> Promise<>
|
|
402
|
+
async _update(key, value, sortValue, index) {
|
|
403
|
+
const item = await this._getItem(index)
|
|
404
|
+
|
|
405
|
+
let forwardIndex = item.forwardIndex
|
|
406
|
+
let reverseIndex = item.reverseIndex
|
|
407
|
+
|
|
408
|
+
if (sortValue != item.sortValue) {
|
|
409
|
+
if (item.reverseIndex == -1) { // item to update is first in the list
|
|
410
|
+
if (item.forwardIndex > -1) { // there is an item behind item to update
|
|
411
|
+
await this._updateReverseIndex(item.forwardIndex, -1)
|
|
412
|
+
await this._writeFirstIndex(item.forwardIndex)
|
|
413
|
+
} else { // item to update is first and last in the list
|
|
414
|
+
await this._writeFirstIndex(-1)
|
|
415
|
+
await this._writeLastIndex(-1)
|
|
416
|
+
}
|
|
417
|
+
} else if (item.forwardIndex == -1) { // item to update is last in the list
|
|
418
|
+
if (item.reverseIndex > -1) { // there is an item ahead of item to update
|
|
419
|
+
await this._updateForwardIndex(item.reverseIndex, -1)
|
|
420
|
+
await this._writeLastIndex(item.forwardIndex)
|
|
421
|
+
} else { // item to update is first and last in the list
|
|
422
|
+
}
|
|
423
|
+
} else { // item to update is in the middle of the list
|
|
424
|
+
await this._updateReverseIndex(item.forwardIndex, item.reverseIndex)
|
|
425
|
+
await this._updateForwardIndex(item.reverseIndex, item.forwardIndex)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const forwardStartItem = await this._getForwardStartItem()
|
|
429
|
+
let previousForwardItem = null
|
|
430
|
+
let currentForwardItem = forwardStartItem
|
|
431
|
+
while (currentForwardItem) {
|
|
432
|
+
const left = typeof sortValue == 'string' ? currentForwardItem.sortValue : Number(currentForwardItem.sortValue)
|
|
433
|
+
if (sortValue > left) {
|
|
434
|
+
previousForwardItem = currentForwardItem
|
|
435
|
+
currentForwardItem = await this._getItem(previousForwardItem.forwardIndex)
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (previousForwardItem == null) { // item to update is first in the list
|
|
442
|
+
reverseIndex = -1
|
|
443
|
+
await this._writeFirstIndex(index)
|
|
444
|
+
if (forwardStartItem == null) { // item to update is also last in the list
|
|
445
|
+
forwardIndex = -1
|
|
446
|
+
await this._writeLastIndex(index)
|
|
447
|
+
} else {
|
|
448
|
+
forwardIndex = forwardStartItem.index
|
|
449
|
+
await this._updateReverseIndex(forwardStartItem.index, index)
|
|
450
|
+
}
|
|
451
|
+
} else if (previousForwardItem.forwardIndex == -1) { // item to insert is the last in the list
|
|
452
|
+
forwardIndex = -1
|
|
453
|
+
await this._writeLastIndex(index)
|
|
454
|
+
await this._updateForwardIndex(previousForwardItem.index, index)
|
|
455
|
+
reverseIndex = previousForwardItem.index
|
|
456
|
+
} else { // item to insert is ahead of previousForwardItem and there was an item ahead of previousForwardItem
|
|
457
|
+
await this._updateForwardIndex(previousForwardItem.index, index)
|
|
458
|
+
await this._updateReverseIndex(currentForwardItem.index, index)
|
|
459
|
+
forwardIndex = previousForwardItem.forwardIndex
|
|
460
|
+
reverseIndex = previousForwardItem.index
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const position = index * DATA_SLICE_SIZE
|
|
466
|
+
const buffer = Buffer.alloc(DATA_SLICE_SIZE)
|
|
467
|
+
const sortValueString = typeof sortValue == 'string' ? sortValue : sortValue.toString()
|
|
468
|
+
|
|
469
|
+
// 8 bits / 1 byte for status marker: 0 empty / 1 occupied / 2 deleted
|
|
470
|
+
// 32 bits / 4 bytes for key size
|
|
471
|
+
// 32 bits / 4 bytes for sort value size
|
|
472
|
+
// 32 bits / 4 bytes for value size
|
|
473
|
+
// 32 bits / 4 bytes for forward index
|
|
474
|
+
// 32 bits / 4 bytes for reverse index
|
|
475
|
+
// chunk for key
|
|
476
|
+
// chunk for sort value
|
|
477
|
+
// remainder for value
|
|
478
|
+
const statusMarker = 1
|
|
479
|
+
const keyByteLength = Buffer.byteLength(key, ENCODING)
|
|
480
|
+
const sortValueByteLength = Buffer.byteLength(sortValueString, ENCODING)
|
|
481
|
+
const valueByteLength = Buffer.byteLength(value, ENCODING)
|
|
482
|
+
buffer.writeUInt8(statusMarker, 0)
|
|
483
|
+
buffer.writeUint32BE(keyByteLength, 1)
|
|
484
|
+
buffer.writeUint32BE(sortValueByteLength, 5)
|
|
485
|
+
buffer.writeUint32BE(valueByteLength, 9)
|
|
486
|
+
buffer.writeInt32BE(forwardIndex, 13)
|
|
487
|
+
buffer.writeInt32BE(reverseIndex, 17)
|
|
488
|
+
buffer.write(key, 21, keyByteLength, ENCODING)
|
|
489
|
+
buffer.write(sortValueString, 21 + keyByteLength, sortValueByteLength, ENCODING)
|
|
490
|
+
buffer.write(value, 21 + keyByteLength + sortValueByteLength, valueByteLength, ENCODING)
|
|
491
|
+
|
|
492
|
+
await this.storageFd.write(buffer, {
|
|
493
|
+
offset: 0,
|
|
494
|
+
position,
|
|
495
|
+
length: buffer.length,
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @name set
|
|
501
|
+
*
|
|
502
|
+
* @docs
|
|
503
|
+
* ```coffeescript [specscript]
|
|
504
|
+
* set(
|
|
505
|
+
* key string,
|
|
506
|
+
* value string,
|
|
507
|
+
* sortValue string|number
|
|
508
|
+
* ) -> Promise<>
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
async set(key, value, sortValue) {
|
|
512
|
+
let index = this._hash1(key)
|
|
513
|
+
|
|
514
|
+
const startIndex = index
|
|
515
|
+
const stepSize = this._hash2(key)
|
|
516
|
+
|
|
517
|
+
let currentKey = await this._getKey(index)
|
|
518
|
+
while (currentKey) {
|
|
519
|
+
if (key == currentKey) {
|
|
520
|
+
break
|
|
521
|
+
}
|
|
522
|
+
index = (index + stepSize) % this._length
|
|
523
|
+
if (index == startIndex) {
|
|
524
|
+
throw new Error('Hash table is full')
|
|
525
|
+
}
|
|
526
|
+
currentKey = await this._getKey(index)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (currentKey == null) {
|
|
530
|
+
await this._insert(key, value, sortValue, index)
|
|
531
|
+
await this._incrementCount()
|
|
532
|
+
} else {
|
|
533
|
+
await this._update(key, value, sortValue, index)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// get(key string) -> value Promise<string>
|
|
538
|
+
async get(key) {
|
|
539
|
+
let index = this._hash1(key)
|
|
540
|
+
const startIndex = index
|
|
541
|
+
const stepSize = this._hash2(key)
|
|
542
|
+
|
|
543
|
+
let currentKey = await this._getKey(index)
|
|
544
|
+
while (currentKey) {
|
|
545
|
+
if (key == currentKey) {
|
|
546
|
+
break
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
index = (index + stepSize) % this._length
|
|
550
|
+
if (index == startIndex) {
|
|
551
|
+
return undefined // entire table searched
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
currentKey = await this._getKey(index)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (currentKey == null) {
|
|
558
|
+
return undefined
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const readBuffer = await this._read(index)
|
|
562
|
+
|
|
563
|
+
const statusMarker = readBuffer.readUInt8(0)
|
|
564
|
+
if (statusMarker === OCCUPIED) {
|
|
565
|
+
const keyByteLength = readBuffer.readUInt32BE(1)
|
|
566
|
+
const sortValueByteLength = readBuffer.readUInt32BE(5)
|
|
567
|
+
const valueByteLength = readBuffer.readUInt32BE(9)
|
|
568
|
+
const valueBuffer = readBuffer.subarray(
|
|
569
|
+
21 + keyByteLength + sortValueByteLength,
|
|
570
|
+
21 + keyByteLength + sortValueByteLength + valueByteLength
|
|
571
|
+
)
|
|
572
|
+
return valueBuffer.toString(ENCODING)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return undefined
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// forwardIterator() -> values AsyncGenerator<string>
|
|
579
|
+
async * forwardIterator() {
|
|
580
|
+
let currentForwardItem = await this._getForwardStartItem()
|
|
581
|
+
while (currentForwardItem) {
|
|
582
|
+
yield currentForwardItem.value
|
|
583
|
+
currentForwardItem = await this._getItem(currentForwardItem.forwardIndex)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// reverseIterator() -> values AsyncGenerator<string>
|
|
588
|
+
async * reverseIterator() {
|
|
589
|
+
let currentReverseItem = await this._getReverseStartItem()
|
|
590
|
+
while (currentReverseItem) {
|
|
591
|
+
yield currentReverseItem.value
|
|
592
|
+
currentReverseItem = await this._getItem(currentReverseItem.reverseIndex)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* @name delete
|
|
598
|
+
*
|
|
599
|
+
* @docs
|
|
600
|
+
* ```coffeescript [specscript]
|
|
601
|
+
* delete(key string) -> didDelete Promise<boolean>
|
|
602
|
+
* ```
|
|
603
|
+
*
|
|
604
|
+
* Deletes a key and corresponding value from the disk linked hash table.
|
|
605
|
+
*
|
|
606
|
+
* Arguments:
|
|
607
|
+
* * `key` - `string` - the key to delete.
|
|
608
|
+
*
|
|
609
|
+
* Return:
|
|
610
|
+
* * `didDelete` - `boolean` - a promise of whether the key and corresponding value was deleted.
|
|
611
|
+
*
|
|
612
|
+
* ```javascript
|
|
613
|
+
* const didDelete = await ht.delete('my-key')
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
async delete(key) {
|
|
617
|
+
let index = this._hash1(key)
|
|
618
|
+
const startIndex = index
|
|
619
|
+
const stepSize = this._hash2(key)
|
|
620
|
+
|
|
621
|
+
let currentKey = await this._getKey(index)
|
|
622
|
+
while (currentKey) {
|
|
623
|
+
if (key == currentKey) {
|
|
624
|
+
break
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
index = (index + stepSize) % this._length
|
|
628
|
+
if (index == startIndex) {
|
|
629
|
+
return false // entire table searched
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
currentKey = await this._getKey(index)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (currentKey == null) {
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const item = await this._getItem(index)
|
|
640
|
+
|
|
641
|
+
if (item.reverseIndex == -1) { // item to delete is first in the list
|
|
642
|
+
if (item.forwardIndex > -1) { // there is an item behind item to delete
|
|
643
|
+
await this._updateReverseIndex(item.forwardIndex, -1)
|
|
644
|
+
await this._writeFirstIndex(item.forwardIndex)
|
|
645
|
+
} else { // item to remove is first and last in the list
|
|
646
|
+
await this._writeFirstIndex(-1)
|
|
647
|
+
await this._writeLastIndex(-1)
|
|
648
|
+
}
|
|
649
|
+
} else if (item.forwardIndex == -1) { // item to delete is last in the list
|
|
650
|
+
if (item.reverseIndex > -1) { // there is an item ahead of item to delete
|
|
651
|
+
await this._updateForwardIndex(item.reverseIndex, -1)
|
|
652
|
+
await this._writeLastIndex(item.forwardIndex)
|
|
653
|
+
} else { // item is first and last in the list (handled above)
|
|
654
|
+
}
|
|
655
|
+
} else { // item to delete is in the middle of the list
|
|
656
|
+
await this._updateReverseIndex(item.forwardIndex, item.reverseIndex)
|
|
657
|
+
await this._updateForwardIndex(item.reverseIndex, item.forwardIndex)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (item.statusMarker === OCCUPIED) {
|
|
661
|
+
await this._setStatusMarker(index, REMOVED)
|
|
662
|
+
await this._decrementCount()
|
|
663
|
+
return true
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return false
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// _incrementCount() -> Promise<>
|
|
670
|
+
async _incrementCount() {
|
|
671
|
+
this._count += 1
|
|
672
|
+
|
|
673
|
+
const position = 4
|
|
674
|
+
const buffer = Buffer.alloc(4)
|
|
675
|
+
buffer.writeInt32BE(this._count, 0)
|
|
676
|
+
|
|
677
|
+
await this.headerFd.write(buffer, {
|
|
678
|
+
offset: 0,
|
|
679
|
+
position,
|
|
680
|
+
length: 4,
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// _decrementCount() -> Promise<>
|
|
685
|
+
async _decrementCount() {
|
|
686
|
+
this._count -= 1
|
|
687
|
+
|
|
688
|
+
const position = 4
|
|
689
|
+
const buffer = Buffer.alloc(4)
|
|
690
|
+
buffer.writeInt32BE(this._count, 0)
|
|
691
|
+
|
|
692
|
+
await this.headerFd.write(buffer, {
|
|
693
|
+
offset: 0,
|
|
694
|
+
position,
|
|
695
|
+
length: 4,
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* @name count
|
|
701
|
+
*
|
|
702
|
+
* @docs
|
|
703
|
+
* ```coffeescript [specscript]
|
|
704
|
+
* count() -> number
|
|
705
|
+
* ```
|
|
706
|
+
*
|
|
707
|
+
* Returns the number of items (key-value pairs) in the disk linked hash table.
|
|
708
|
+
*
|
|
709
|
+
* Arguments:
|
|
710
|
+
* * (none)
|
|
711
|
+
*
|
|
712
|
+
* Return:
|
|
713
|
+
* * `number` - the number of items in the disk hash table.
|
|
714
|
+
*
|
|
715
|
+
* ```javascript
|
|
716
|
+
* const count = ht.count()
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
count() {
|
|
720
|
+
return this._count
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
module.exports = DiskSortedHashTable
|
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The CLOUT Free and Open Source Software License (CFOSS)
|
|
2
|
+
|
|
3
|
+
© Richard Yufei Tong, King of Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
1. This permission notice is included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE, OR IN THE USE OF OR OTHER DEALINGS OF THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
// getPhysicalSectorSize(device string) -> number
|
|
4
|
+
function getPhysicalSectorSize(device) {
|
|
5
|
+
try {
|
|
6
|
+
const path = `/sys/block/${device}/queue/physical_block_size`
|
|
7
|
+
const size = fs.readFileSync(path, 'utf8')
|
|
8
|
+
return parseInt(size.trim(), 10)
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new Error('Could not read sector size:', error.message)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = getPhysicalSectorSize
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
// listDiskDevices() -> devices Array<string>
|
|
4
|
+
function listDiskDevices() {
|
|
5
|
+
try {
|
|
6
|
+
const devices = fs.readdirSync('/sys/block')
|
|
7
|
+
return devices
|
|
8
|
+
} catch (error) {
|
|
9
|
+
throw new Error('Error reading /sys/block:', error.message)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = listDiskDevices
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const { execSync } = require('child_process')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pre-allocates a file using the 'fallocate' system utility.
|
|
6
|
+
* This is extremely fast and ensures contiguous disk blocks.
|
|
7
|
+
*/
|
|
8
|
+
function preallocate(filePath, sizeInBytes) {
|
|
9
|
+
execSync(`fallocate -l ${sizeInBytes} ${filePath}`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = preallocate
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "presidium-db",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Presidium DB library",
|
|
5
|
+
"author": "Richard Tong",
|
|
6
|
+
"license": "CFOSS",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "github:richytong/presidium-db"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/richytong/presidium-db",
|
|
12
|
+
"files": [
|
|
13
|
+
"_internal",
|
|
14
|
+
"DiskHashTable.js",
|
|
15
|
+
"DiskSortedHashTable.js",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"hash",
|
|
21
|
+
"hashtable",
|
|
22
|
+
"map",
|
|
23
|
+
"database",
|
|
24
|
+
"nosql",
|
|
25
|
+
"node",
|
|
26
|
+
"nodejs",
|
|
27
|
+
"javascript",
|
|
28
|
+
"presidium"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "mocha test.js **/*test.js",
|
|
32
|
+
"bench": "./bench"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"nyc": "^18.0.0",
|
|
36
|
+
"thunk-test": "^1.3.9"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@ronomon/direct-io": "^3.0.1"
|
|
40
|
+
}
|
|
41
|
+
}
|