hypercore 11.27.16 → 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 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
  }
@@ -962,6 +1049,7 @@ class Hypercore extends EventEmitter {
962
1049
 
963
1050
  async truncate(newLength = 0, opts = {}) {
964
1051
  if (this.opened === false) await this.opening
1052
+ if (this.closing) throw SESSION_CLOSED('Cannot append to a closed session', this.discoveryKey)
965
1053
 
966
1054
  const {
967
1055
  fork = this.state.fork + 1,
@@ -983,6 +1071,7 @@ class Hypercore extends EventEmitter {
983
1071
 
984
1072
  async append(blocks, opts = {}) {
985
1073
  if (this.opened === false) await this.opening
1074
+ if (this.closing) throw SESSION_CLOSED('Cannot append to a closed session', this.discoveryKey)
986
1075
 
987
1076
  const isDefault = this.state === this.core.state
988
1077
  const defaultKeyPair = this.state.name === null ? this.keyPair : null
@@ -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.27.16",
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.0.0",
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",