hypercore 11.27.17 → 11.28.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/README.md +48 -0
- package/index.js +87 -0
- package/lib/mark-bitfield.js +108 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -522,6 +522,54 @@ Info {
|
|
|
522
522
|
}
|
|
523
523
|
```
|
|
524
524
|
|
|
525
|
+
#### `await core.startMarking()`
|
|
526
|
+
|
|
527
|
+
This enables marking mode for the "mark & sweep" approach to clear hypercore storage. When called the current markings are cleared.
|
|
528
|
+
|
|
529
|
+
##### Mark & Sweep
|
|
530
|
+
|
|
531
|
+
This technique allows for marking blocks that should be kept and assuming all other blocks should be cleared. It can be achieved using the following steps:
|
|
532
|
+
|
|
533
|
+
1. Enable marking mode via `await core.startMarking()`.
|
|
534
|
+
2. Get all blocks that should be kept.
|
|
535
|
+
While the marking mode is enabled, all blocks retrieved (via `.get()`, etc) will be "marked". Marked blocks will not be cleared when sweeping.
|
|
536
|
+
3. Sweep to clear unmarked blocks via `await core.sweep()`.
|
|
537
|
+
Once complete, all blocks that were not marked will be cleared.
|
|
538
|
+
|
|
539
|
+
> [!CAUTION]
|
|
540
|
+
> Be careful that caching does not skip a call to `.get()`.
|
|
541
|
+
> For example, `hyperbee` has caches for looking up the b-tree nodes that
|
|
542
|
+
> needs to be cleared before using mark & sweep.
|
|
543
|
+
|
|
544
|
+
Example:
|
|
545
|
+
|
|
546
|
+
```js
|
|
547
|
+
await core.startMarking()
|
|
548
|
+
await core.get(2)
|
|
549
|
+
await core.get(4)
|
|
550
|
+
await core.sweep() // All blocks but blocks 2 & 4 are cleared
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### `await core.markBlock(start, end = start + 1)`
|
|
554
|
+
|
|
555
|
+
Manually mark a block or range of blocks to be retained when sweeping. Useful to mark blocks without loading them into memory. `end` is non-inclusive and defaults to `start + 1` so `core.markBlock(index)` only marks the block at `index`.
|
|
556
|
+
|
|
557
|
+
#### `await core.clearMarkings()`
|
|
558
|
+
|
|
559
|
+
Manually remove all markings. Automatically called when calling `core.startMarking()`.
|
|
560
|
+
|
|
561
|
+
#### `await core.sweep(opts)`
|
|
562
|
+
|
|
563
|
+
Clear all unmarked blocks from storage.
|
|
564
|
+
|
|
565
|
+
`opts` can include:
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
{
|
|
569
|
+
batchSize: 1000 // How frequently to flush clears to storage.
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
525
573
|
#### `await core.close([{ error }])`
|
|
526
574
|
|
|
527
575
|
Fully close this core. Passing an error via `{ error }` is optional and all pending replicator requests will be rejected with the error.
|
package/index.js
CHANGED
|
@@ -10,9 +10,11 @@ const id = require('hypercore-id-encoding')
|
|
|
10
10
|
const safetyCatch = require('safety-catch')
|
|
11
11
|
const unslab = require('unslab')
|
|
12
12
|
const flat = require('flat-tree')
|
|
13
|
+
const assert = require('nanoassert')
|
|
13
14
|
|
|
14
15
|
const { SMALL_WANTS } = require('./lib/feature-flags')
|
|
15
16
|
const { UPDATE_COMPAT } = require('./lib/wants')
|
|
17
|
+
const MarkBitfield = require('./lib/mark-bitfield')
|
|
16
18
|
|
|
17
19
|
const inspect = require('./lib/inspect')
|
|
18
20
|
const Core = require('./lib/core')
|
|
@@ -92,6 +94,10 @@ class Hypercore extends EventEmitter {
|
|
|
92
94
|
|
|
93
95
|
this.waits = 0
|
|
94
96
|
|
|
97
|
+
// Mark & Sweep GC
|
|
98
|
+
this._marking = false
|
|
99
|
+
this._marks = null
|
|
100
|
+
|
|
95
101
|
this._sessionIndex = -1
|
|
96
102
|
this._stateIndex = -1 // maintained by session state
|
|
97
103
|
this._monitorIndex = -1 // maintained by replication state
|
|
@@ -218,6 +224,8 @@ class Hypercore extends EventEmitter {
|
|
|
218
224
|
const onseq = opts.onseq === undefined ? this.onseq : opts.onseq
|
|
219
225
|
const timeout = opts.timeout === undefined ? this.timeout : opts.timeout
|
|
220
226
|
const weak = opts.weak === undefined ? this.weak : opts.weak
|
|
227
|
+
const marking = this._marking
|
|
228
|
+
const marks = this._marks
|
|
221
229
|
const Clz = opts.class || Hypercore
|
|
222
230
|
const s = new Clz(null, this.key, {
|
|
223
231
|
...opts,
|
|
@@ -229,6 +237,8 @@ class Hypercore extends EventEmitter {
|
|
|
229
237
|
weak,
|
|
230
238
|
parent: this
|
|
231
239
|
})
|
|
240
|
+
s._marking = marking
|
|
241
|
+
s._marks = marks
|
|
232
242
|
|
|
233
243
|
return s
|
|
234
244
|
}
|
|
@@ -824,6 +834,7 @@ class Hypercore extends EventEmitter {
|
|
|
824
834
|
(opts && opts.valueEncoding && c.from(opts.valueEncoding)) || this.valueEncoding
|
|
825
835
|
|
|
826
836
|
if (this.onseq !== null) this.onseq(index, this)
|
|
837
|
+
if (this._marking) await this.markBlock(index)
|
|
827
838
|
|
|
828
839
|
const req = this._get(index, opts)
|
|
829
840
|
|
|
@@ -934,6 +945,82 @@ class Hypercore extends EventEmitter {
|
|
|
934
945
|
return defaultValue
|
|
935
946
|
}
|
|
936
947
|
|
|
948
|
+
_setupMarks() {
|
|
949
|
+
if (this._marks === null) {
|
|
950
|
+
const storage = this.snapshotted ? this.core.state.storage : this.state.storage
|
|
951
|
+
this._marks = new MarkBitfield(storage)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async markBlock(start, end = start + 1) {
|
|
956
|
+
if (this.opened === false) await this.opening
|
|
957
|
+
|
|
958
|
+
this._setupMarks()
|
|
959
|
+
|
|
960
|
+
// TODO support as single rocks batch
|
|
961
|
+
const setPromises = []
|
|
962
|
+
for (let i = start; i < end; i++) {
|
|
963
|
+
setPromises.push(this._marks.set(i, true))
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return Promise.all(setPromises)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async clearMarkings() {
|
|
970
|
+
if (this.opened === false) await this.opening
|
|
971
|
+
|
|
972
|
+
this._setupMarks()
|
|
973
|
+
|
|
974
|
+
await this._marks.clear()
|
|
975
|
+
this._marks = null
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async startMarking() {
|
|
979
|
+
if (this._marking) {
|
|
980
|
+
throw ASSERTION("Hypercore cannot be gc'ed when already in gc mode", this.discoveryKey)
|
|
981
|
+
}
|
|
982
|
+
if (this.state && this.state.name) {
|
|
983
|
+
throw ASSERTION("Hypercore cannot be gc'ed when a named session", this.discoveryKey)
|
|
984
|
+
}
|
|
985
|
+
if (this.state && this.state.storage.atom) {
|
|
986
|
+
throw ASSERTION("Hypercore cannot be gc'ed when an atomic session", this.discoveryKey)
|
|
987
|
+
}
|
|
988
|
+
if (this.opened === false) await this.opening
|
|
989
|
+
await this.clearMarkings()
|
|
990
|
+
|
|
991
|
+
this._marking = true
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async sweep({ batchSize = 1000 } = {}) {
|
|
995
|
+
if (this.opened === false) await this.opening
|
|
996
|
+
|
|
997
|
+
assert(!this.snapshotted, 'Cannot sweep a snapshot')
|
|
998
|
+
|
|
999
|
+
// No marks - load from storage
|
|
1000
|
+
this._setupMarks()
|
|
1001
|
+
|
|
1002
|
+
let clearing = []
|
|
1003
|
+
let prevIndex = this.length
|
|
1004
|
+
for await (const index of this._marks.createMarkStream({ reverse: true })) {
|
|
1005
|
+
if (index + 1 === prevIndex) {
|
|
1006
|
+
prevIndex = index
|
|
1007
|
+
continue
|
|
1008
|
+
}
|
|
1009
|
+
clearing.push(this.clear(index + 1, prevIndex))
|
|
1010
|
+
if (clearing.length >= batchSize) {
|
|
1011
|
+
await Promise.all(clearing)
|
|
1012
|
+
clearing = []
|
|
1013
|
+
}
|
|
1014
|
+
prevIndex = index
|
|
1015
|
+
}
|
|
1016
|
+
// Clear range from the very start if not marked
|
|
1017
|
+
if (prevIndex > 0) clearing.push(this.clear(0, prevIndex))
|
|
1018
|
+
await Promise.all(clearing)
|
|
1019
|
+
|
|
1020
|
+
this._marking = false
|
|
1021
|
+
await this.clearMarkings()
|
|
1022
|
+
}
|
|
1023
|
+
|
|
937
1024
|
createReadStream(opts) {
|
|
938
1025
|
return new ReadStream(this, opts)
|
|
939
1026
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const BigSparseArray = require('big-sparse-array')
|
|
2
|
+
const { Transform } = require('streamx')
|
|
3
|
+
const quickbit = require('./compat').quickbit
|
|
4
|
+
const b4a = require('b4a')
|
|
5
|
+
|
|
6
|
+
const BITS_PER_PAGE = 32768
|
|
7
|
+
const BYTES_PER_PAGE = BITS_PER_PAGE / 8
|
|
8
|
+
|
|
9
|
+
class MarkPage {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.bitfield = null
|
|
12
|
+
this.loaded = new Promise((resolve) => {
|
|
13
|
+
this.load = resolve
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setBitfield(bitfield) {
|
|
18
|
+
this.bitfield = bitfield
|
|
19
|
+
this.load()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(index) {
|
|
23
|
+
return quickbit.get(this.bitfield, index)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set(index, val) {
|
|
27
|
+
quickbit.set(this.bitfield, index, val)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = class MarkBitfield {
|
|
32
|
+
static BITS_PER_PAGE = BITS_PER_PAGE
|
|
33
|
+
static BYTES_PER_PAGE = BYTES_PER_PAGE
|
|
34
|
+
|
|
35
|
+
constructor(storage) {
|
|
36
|
+
this.storage = storage
|
|
37
|
+
this._pages = new BigSparseArray()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async loadPage(pageIndex) {
|
|
41
|
+
const p = this._pages.set(pageIndex, new MarkPage())
|
|
42
|
+
const rx = this.storage.read()
|
|
43
|
+
const pageBuf = rx.getMark(pageIndex)
|
|
44
|
+
rx.tryFlush()
|
|
45
|
+
const bitfield = (await pageBuf) ?? b4a.alloc(BYTES_PER_PAGE)
|
|
46
|
+
await p.setBitfield(bitfield)
|
|
47
|
+
return p
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async get(index) {
|
|
51
|
+
const j = index & (BITS_PER_PAGE - 1)
|
|
52
|
+
const i = (index - j) / BITS_PER_PAGE
|
|
53
|
+
|
|
54
|
+
let p = this._pages.get(i)
|
|
55
|
+
if (!p) p = await this.loadPage(i)
|
|
56
|
+
|
|
57
|
+
return p.get(j)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async set(index, val) {
|
|
61
|
+
const j = index & (BITS_PER_PAGE - 1)
|
|
62
|
+
const i = (index - j) / BITS_PER_PAGE
|
|
63
|
+
|
|
64
|
+
let p = this._pages.get(i)
|
|
65
|
+
|
|
66
|
+
if (!p && val) p = await this.loadPage(i)
|
|
67
|
+
|
|
68
|
+
if (p) {
|
|
69
|
+
await p.loaded
|
|
70
|
+
p.set(j, val)
|
|
71
|
+
const tx = this.storage.write()
|
|
72
|
+
tx.putMark(i, p.bitfield)
|
|
73
|
+
await tx.flush()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async clear() {
|
|
78
|
+
const tx = this.storage.write()
|
|
79
|
+
tx.deleteMarkRange(0, -1)
|
|
80
|
+
await tx.flush()
|
|
81
|
+
this._pages = new BigSparseArray()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
createMarkStream({ reverse = false } = {}) {
|
|
85
|
+
return this.storage.createMarkStream({ reverse }).pipe(
|
|
86
|
+
new Transform({
|
|
87
|
+
transform({ index, page }, cb) {
|
|
88
|
+
let bitIndex = reverse
|
|
89
|
+
? quickbit.findLast(page, true, BITS_PER_PAGE)
|
|
90
|
+
: quickbit.findFirst(page, true, 0)
|
|
91
|
+
while (bitIndex !== -1) {
|
|
92
|
+
const blockIndex = index * BITS_PER_PAGE + bitIndex
|
|
93
|
+
this.push(blockIndex)
|
|
94
|
+
|
|
95
|
+
// Account for `bitIndex` being either causing infinite loop
|
|
96
|
+
if (bitIndex === 0 && reverse) break
|
|
97
|
+
|
|
98
|
+
bitIndex = reverse
|
|
99
|
+
? quickbit.findLast(page, true, bitIndex - 1)
|
|
100
|
+
: quickbit.findFirst(page, true, bitIndex + 1)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cb(null)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.28.0",
|
|
4
4
|
"description": "Hypercore is a secure, distributed append-only log",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"hypercore-crypto": "^3.2.1",
|
|
55
55
|
"hypercore-errors": "^1.5.0",
|
|
56
56
|
"hypercore-id-encoding": "^1.2.0",
|
|
57
|
-
"hypercore-storage": "^2.
|
|
57
|
+
"hypercore-storage": "^2.8.0",
|
|
58
58
|
"is-options": "^1.0.1",
|
|
59
59
|
"nanoassert": "^2.0.0",
|
|
60
60
|
"protomux": "^3.5.0",
|