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.
@@ -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,4 @@
1
+ # presidium-db
2
+
3
+ # Supported Platforms
4
+ * Linux
@@ -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
+ }