autobee 2.0.0-rc.2 → 2.0.0-rc.21

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/lib/topo.js CHANGED
@@ -6,6 +6,8 @@ const asserts = require('./asserts.js')
6
6
  const EMPTY_HEAD = { length: 0, key: null }
7
7
  const INFO_LEGACY_KEY = b4a.concat([b4a.from([0, 0]), b4a.from('info')])
8
8
 
9
+ const DEFAULT_OP_TIMEOUT = 5000
10
+
9
11
  exports.sort = sort
10
12
  exports.rollback = rollback
11
13
  exports.replay = replay
@@ -13,6 +15,7 @@ exports.getMostRecentHead = getMostRecentHead
13
15
 
14
16
  exports.isLinking = isLinking
15
17
  exports.isLinkingAll = isLinkingAll
18
+ exports.isLegacyNode = isLegacyNode
16
19
 
17
20
  class Clock {
18
21
  constructor() {
@@ -35,26 +38,46 @@ class Clock {
35
38
  async function getMostRecentHead(auto, system) {
36
39
  const ch = system.createChangesStream()
37
40
  for await (const data of ch) {
38
- const { ref } = getOplog(data)
39
- return { key: ref.oplog.key, length: ref.oplog.length }
41
+ const refs = getOplogCompat(data)
42
+ if (!refs.length) continue
43
+
44
+ const { oplog } = refs[0]
45
+ return { key: oplog.key, length: oplog.length }
40
46
  }
47
+
48
+ return null
41
49
  }
42
50
 
43
51
  async function rollback(auto, system, verified) {
52
+ if (!(await isApplied(system, verified.op))) return null
53
+
44
54
  const tip = []
45
55
  const ch = system.createChangesStream()
46
56
 
57
+ let found = false
58
+
47
59
  for await (const data of ch) {
48
60
  const { ref, info } = getOplog(data)
61
+
49
62
  if (sameNode(ref.oplog, verified.op)) {
50
- asserts.assert(info.flushes === verified.flushes, 'Expect tip to be continuous')
63
+ // stale views.flushes - the verified head describes a prefix this system
64
+ // does not share, so nothing above it is ours to reapply
65
+ if (info.flushes === verified.flushes) found = true
66
+ break
67
+ }
68
+
69
+ // walking back, a writer's lengths only decrease - once this writer is
70
+ // behind the verified head the marker cannot be further down
71
+ if (b4a.equals(ref.oplog.key, verified.op.key) && ref.oplog.length < verified.op.length) {
51
72
  break
52
73
  }
53
74
 
54
75
  tip.push(ref)
55
76
  }
56
77
 
57
- const inflated = await inflate(auto, tip)
78
+ if (!found) return null
79
+
80
+ const inflated = await inflate(auto, tip.reverse())
58
81
 
59
82
  // @todo: verify sort
60
83
 
@@ -65,17 +88,32 @@ async function rollback(auto, system, verified) {
65
88
  }
66
89
  }
67
90
 
91
+ // nodes we cannot resolve are returned as null rather than dropped, so callers
92
+ // can tell an incomplete replay from a short one
68
93
  async function replay(auto) {
69
- const ch = auto.system.bee.createChangesStream()
70
94
  const replay = []
71
95
 
72
- for await (const data of ch) {
73
- const { ref } = getOplog(data)
74
- replay.push(ref)
96
+ for await (const entry of oplogIterator(auto.system.bee)) {
97
+ // the walk was cut short, nothing below this point is reachable
98
+ if (entry === null) {
99
+ replay.push(null)
100
+ break
101
+ }
102
+
103
+ for (const ref of entry.refs) replay.push(ref)
75
104
  }
76
105
 
77
- const inflated = await inflate(auto, replay.reverse())
78
- return inflated.flatMap((b) => b.batch)
106
+ // same reasoning as the iterator - never block on what we do not have
107
+ const inflated = await inflate(auto, replay.reverse(), { wait: false })
108
+ return inflated.flatMap((b) => (b === null ? [null] : b.batch))
109
+ }
110
+
111
+ async function isApplied(system, node) {
112
+ const entry = await system.get(encoding.encodeSystemWriterKey(node.key))
113
+ if (entry === null) return false
114
+
115
+ const info = encoding.decodeSystemWriter(entry.key, entry.value)
116
+ return info.length >= node.length
79
117
  }
80
118
 
81
119
  async function sort(auto, batch) {
@@ -192,12 +230,21 @@ function cmp(a, b) {
192
230
  return a.length < b.length ? -1 : 1
193
231
  }
194
232
 
195
- async function inflate(auto, reorder) {
233
+ async function inflate(auto, reorder, opts = null) {
234
+ const readOpts = opts === null ? { timeout: DEFAULT_OP_TIMEOUT } : opts
235
+
196
236
  const promises = []
197
237
  const sessions = new Map()
198
238
 
199
239
  for (let i = 0; i < reorder.length; i++) {
200
240
  const ref = reorder[i]
241
+
242
+ // marker for a node we could not resolve, pass it through
243
+ if (ref === null) {
244
+ promises.push(null)
245
+ continue
246
+ }
247
+
201
248
  const id = b4a.toString(ref.oplog.key, 'hex')
202
249
  // only seeds the node - reapplication re-resolves from the witness
203
250
  const weight = ref.oplog.weight
@@ -214,7 +261,7 @@ async function inflate(auto, reorder) {
214
261
  sessions.set(id, core)
215
262
  }
216
263
 
217
- promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i))
264
+ promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, readOpts))
218
265
  }
219
266
 
220
267
  try {
@@ -224,9 +271,10 @@ async function inflate(auto, reorder) {
224
271
  }
225
272
  }
226
273
 
227
- async function getOplogBatch(undo, core, length, weight, index) {
274
+ async function getOplogBatch(undo, core, length, weight, index, opts = null) {
228
275
  const seq = length - 1
229
- const block = await core.get(seq)
276
+ const block = await core.get(seq, opts)
277
+ if (block === null) return null
230
278
  const oplog = encoding.decodeOplog(block)
231
279
  const head = createNode(core, length, weight, oplog)
232
280
 
@@ -238,12 +286,18 @@ async function getOplogBatch(undo, core, length, weight, index) {
238
286
  const end = seq + head.batch.end // skip head, we have it
239
287
 
240
288
  for (let i = start; i < end; i++) {
241
- remaining.push(core.get(i))
289
+ remaining.push(core.get(i, opts))
242
290
  }
243
291
 
244
292
  if (remaining.length) {
245
293
  const blocks = await Promise.all(remaining)
246
294
  for (let i = 0; i < blocks.length; i++) {
295
+ // keep the hole so the gap in the batch stays visible
296
+ if (blocks[i] === null) {
297
+ batch.push(null)
298
+ continue
299
+ }
300
+
247
301
  const oplog = encoding.decodeOplog(blocks[i])
248
302
  batch.push(createNode(core, i + start + 1, weight, oplog))
249
303
  }
@@ -254,8 +308,14 @@ async function getOplogBatch(undo, core, length, weight, index) {
254
308
  }
255
309
 
256
310
  function getLocalBatch(undo, writer, length, weight, index) {
311
+ if (writer.core.closed) throw new Error('Writer core is closed')
312
+
257
313
  if (length > writer.core.length) {
258
- if (!writer.pending) throw new Error('No local nodes')
314
+ if (!writer.pending) {
315
+ throw new Error(
316
+ `No local nodes: looking for ${length}, found ${writer.core.length}@${writer.core.id}`
317
+ )
318
+ }
259
319
 
260
320
  let i = 0
261
321
  while (i < writer.pending.length) {
@@ -264,9 +324,10 @@ function getLocalBatch(undo, writer, length, weight, index) {
264
324
  }
265
325
 
266
326
  const head = writer.pending[i]
327
+ const headBatch = head.batch || { start: 0, end: 0 }
267
328
 
268
- const start = i - head.batch.start
269
- const end = i + head.batch.end + 1 // include head
329
+ const start = i - headBatch.start
330
+ const end = i + headBatch.end + 1 // include head
270
331
 
271
332
  const batch = writer.pending.slice(start, end)
272
333
  return { undo, batch, index }
@@ -275,6 +336,19 @@ function getLocalBatch(undo, writer, length, weight, index) {
275
336
  return getOplogBatch(undo, writer.core, length, weight, index)
276
337
  }
277
338
 
339
+ // tolerates legacy nodes instead of asserting
340
+ async function* oplogIterator(bee) {
341
+ try {
342
+ for await (const node of bee.createChangesStream({ wait: false })) {
343
+ const legacy = isLegacyNode(node)
344
+ const refs = getOplogCompat(node)
345
+ if (refs.length) yield { node, refs, legacy }
346
+ }
347
+ } catch {
348
+ yield null // signal data not available
349
+ }
350
+ }
351
+
278
352
  function getOplog(data) {
279
353
  const result = { ref: null, info: null }
280
354
  for (const { keys } of data.batch) {
@@ -300,6 +374,35 @@ function getOplog(data) {
300
374
  asserts.bail('Bad system node')
301
375
  }
302
376
 
377
+ // legacy nodes carry no isOplog flag so we cannot tell which writer update is
378
+ // the oplog to fetch - yield every candidate until we have a better signal
379
+ function getOplogCompat(data) {
380
+ const undo = data.tail || EMPTY_HEAD
381
+ const oplogs = []
382
+ const candidates = []
383
+
384
+ for (const { keys } of data.batch) {
385
+ for (const k of keys) {
386
+ if (k.key[0] !== 1) continue
387
+
388
+ let value = null
389
+ try {
390
+ value = encoding.decodeSystemWriter(k.key, k.value)
391
+ } catch {
392
+ continue
393
+ }
394
+
395
+ // writer was just added, nothing to fetch
396
+ if (!value.length) continue
397
+
398
+ if (value.isOplog) oplogs.push({ undo, oplog: value })
399
+ else candidates.push({ undo, oplog: value })
400
+ }
401
+ }
402
+
403
+ return oplogs.length ? oplogs : candidates
404
+ }
405
+
303
406
  function isLinking(node, link) {
304
407
  if (node.length >= link.length && b4a.equals(node.key, link.key)) {
305
408
  return true
package/lib/trusted.js ADDED
@@ -0,0 +1,44 @@
1
+ const b4a = require('b4a')
2
+
3
+ class TrustedPeers {
4
+ constructor(hooks) {
5
+ this.trusted = new Map()
6
+ this.pending = new Map()
7
+ this.hooks = hooks
8
+ }
9
+
10
+ async isTrusted(key, reference) {
11
+ const hex = b4a.toString(key, 'hex')
12
+ if (this.trusted.get(hex)) return true
13
+
14
+ let promise = this.pending.get(hex)
15
+ if (promise) return promise
16
+
17
+ promise = this.hooks.isTrusted
18
+ ? this.hooks.isTrusted(key, reference)
19
+ : Promise.resolve(!this.hooks.mostRecentTrusted)
20
+ this.pending.set(hex, promise)
21
+
22
+ const trusted = await promise
23
+ if (trusted) this.trusted.set(hex, trusted)
24
+
25
+ if (this.pending.get(hex) === promise) this.pending.delete(hex)
26
+ return trusted
27
+ }
28
+
29
+ read(trusted) {
30
+ return trusted || null
31
+ }
32
+
33
+ mostRecentTrusted(target, reference) {
34
+ if (!this.hooks.mostRecentTrusted) return Promise.resolve(null)
35
+ return this.hooks.mostRecentTrusted(target, reference)
36
+ }
37
+
38
+ clear() {
39
+ this.pending.clear()
40
+ this.trusted.clear()
41
+ }
42
+ }
43
+
44
+ module.exports = TrustedPeers
package/lib/updates.js CHANGED
@@ -1,5 +1,3 @@
1
- const b4a = require('b4a')
2
-
3
1
  module.exports = class UpdateChanges {
4
2
  constructor(auto) {
5
3
  this.auto = auto
@@ -21,57 +19,22 @@ module.exports = class UpdateChanges {
21
19
  flushes: this.auto.system.flushes,
22
20
  from: this.tracking[k].head(),
23
21
  to: null,
24
- shared: null
22
+ incremental: false
25
23
  })
26
24
  }
27
25
  }
28
26
 
29
27
  finalise() {
30
- const floor = this.auto.system.shared
31
-
32
28
  // flush count at `to` - safe to read live, state is frozen for the hook
33
29
  const flushes = this.auto.system.flushes
34
30
 
35
31
  for (const k in this.tracking) {
36
32
  const tracked = this.byName.get(k)
37
- const startFlushes = tracked.flushes
38
33
 
39
34
  tracked.to = this.tracking[k].head()
40
35
  tracked.flushes = flushes
41
-
42
- if (floor && floor.flushes < startFlushes) {
43
- tracked.shared = k === 'view' ? floor.view : floor.system
44
- } else {
45
- tracked.shared = tracked.from
46
- }
47
36
  }
48
37
 
49
38
  return this.byName
50
39
  }
51
-
52
- static from(shared, current) {
53
- if (
54
- shared.flushes === current.flushes &&
55
- shared.system.length === current.system.length &&
56
- (shared.system.key === current.system.key ||
57
- (shared.system.key &&
58
- current.system.key &&
59
- b4a.equals(shared.system.key, current.system.key)))
60
- ) {
61
- return null
62
- }
63
-
64
- const byName = new Map()
65
- const flushes = current.flushes
66
-
67
- byName.set('view', { from: shared.view, shared: shared.view, to: current.view, flushes })
68
- byName.set('system', {
69
- from: shared.system,
70
- shared: shared.system,
71
- to: current.system,
72
- flushes
73
- })
74
-
75
- return byName
76
- }
77
40
  }
package/lib/writers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const b4a = require('b4a')
2
2
  const encoding = require('./encoding.js')
3
3
  const Triggers = require('./triggers.js')
4
- const { signAttestation } = require('./witness.js')
4
+ const { signAttestation, resolveWeight } = require('./witness.js')
5
5
  const { WriterEncryption } = require('autobee-encryption')
6
6
 
7
7
  const WRITER_PREFETCH = 10
@@ -25,6 +25,8 @@ class ActiveWriters {
25
25
  this.witnesses = []
26
26
  this.attestations = []
27
27
 
28
+ this._pendingBuffer = new Map()
29
+
28
30
  this.localWriter.attach()
29
31
 
30
32
  this._bootstrap()
@@ -62,9 +64,14 @@ class ActiveWriters {
62
64
  const info = await this.auto.system.get(key, { unflushed: true })
63
65
  if (info && info.isAnchor) return
64
66
  w = await this.add(key)
67
+ if (!w) return
65
68
  }
66
69
 
67
- // TODO: signal that we've seen length on w for gc
70
+ // remember the announced length so gc doesn't drop a writer we haven't caught up on
71
+ w.hint(length)
72
+
73
+ // prefetch the oplog head for the fast-forward that runs right after us
74
+ if (w && length > 0) w.core.get(length - 1).catch(noop)
68
75
 
69
76
  this.bump()
70
77
  }
@@ -95,19 +102,24 @@ class ActiveWriters {
95
102
  return this.localWriter.clear()
96
103
  }
97
104
 
98
- flushLocal(views) {
105
+ async flushLocal(views) {
99
106
  const w = this.localWriter
100
107
 
101
108
  // only consume the queue when a flush will actually write
102
- if (w.pending === null || w.processed === 0) return Promise.resolve()
109
+ if (w.pending === null || w.processed === 0) return
103
110
 
104
111
  const queued = this.attestations
105
112
  this.attestations = []
106
113
 
107
- return w.flush(views, queued)
114
+ const trusted = await this.auto.trusted.mostRecentTrusted(this.auto._workingView.view, null)
115
+
116
+ return w.flush(views, queued, trusted)
108
117
  }
109
118
 
110
119
  async refresh() {
120
+ // peeked batches were read against the pre-reboot system, drop them
121
+ this._pendingBuffer.clear()
122
+
111
123
  if (this.localWriter && !this.localWriter.isClosed) {
112
124
  await this.updateLocalState()
113
125
  }
@@ -122,6 +134,33 @@ class ActiveWriters {
122
134
  await this.localWriter.update()
123
135
  }
124
136
 
137
+ async nextPendingNode() {
138
+ for (const w of this.pending.slice()) {
139
+ if (this._pendingBuffer.has(w) || w.isFrozen) continue
140
+
141
+ const batch = await w.next()
142
+ if (batch !== null) this._pendingBuffer.set(w, batch)
143
+ }
144
+
145
+ if (this._pendingBuffer.size === 0) return null
146
+
147
+ let writer = null
148
+ let best = null
149
+
150
+ for (const [w, batch] of this._pendingBuffer) {
151
+ const head = batch[0]
152
+ // -1 marks a node next() has not resolved a sort weight for yet
153
+ if (head.weight < 0) head.weight = await resolveWeight(this.auto, head)
154
+ if (best === null || sortsBefore(head, best[0])) {
155
+ writer = w
156
+ best = batch
157
+ }
158
+ }
159
+
160
+ this._pendingBuffer.delete(writer)
161
+ return { writer, batch: best }
162
+ }
163
+
125
164
  _updateLocalState() {
126
165
  const w = this.localWriter
127
166
 
@@ -210,11 +249,16 @@ class ActiveWriters {
210
249
  const id = b4a.toString(bootstrap.key, 'hex')
211
250
  let w = this.active.get(id)
212
251
  if (w) {
252
+ w.isBootstrap = true
213
253
  w.couple() // couple if we weren't ready first time
214
254
  return
215
255
  }
216
256
 
217
- w = new Writer(this, this.auto.system, false, bootstrap, id)
257
+ const session = bootstrap.session()
258
+ session.setEncryption(this.auto._getEncryptionProvider())
259
+
260
+ w = new Writer(this, this.auto.system, false, session, id)
261
+ w.isBootstrap = true // the root of the autobase, never gc'd
218
262
  w.attach()
219
263
  w.couple()
220
264
  }
@@ -245,6 +289,7 @@ class Writer {
245
289
  this.appendLength = this.core.length + 1
246
290
  this.waiting = null
247
291
  this.download = null
292
+ this.hintedLength = 0
248
293
 
249
294
  this.isPending = false
250
295
  this.isClosed = false
@@ -253,6 +298,7 @@ class Writer {
253
298
  this.isRemoved = false
254
299
  this.isCoupled = false
255
300
  this.isFrozen = false
301
+ this.isBootstrap = false
256
302
 
257
303
  this._onchangeBound = this._onchange.bind(this)
258
304
 
@@ -300,6 +346,8 @@ class Writer {
300
346
  if (!this.isPending) return
301
347
  this.isPending = false
302
348
 
349
+ this.writers._pendingBuffer.delete(this)
350
+
303
351
  const head = this.writers.pending.pop()
304
352
  if (head !== this) {
305
353
  this.writers.pending[this.index] = head
@@ -307,6 +355,11 @@ class Writer {
307
355
  }
308
356
  }
309
357
 
358
+ // a peer announced this length for us, so we're not caught up until we've seen it
359
+ hint(length) {
360
+ if (length > this.hintedLength) this.hintedLength = length
361
+ }
362
+
310
363
  _onchange() {
311
364
  if (this.waiting === null) this.bump()
312
365
  }
@@ -344,10 +397,10 @@ class Writer {
344
397
 
345
398
  const length = await this.update()
346
399
 
347
- // don't drop any local nodes left in buffer
348
- if (length < this.core.length || this.appending) {
400
+ // don't drop any local nodes left in buffer, or work a peer told us about
401
+ if (length < this.core.length || length < this.hintedLength || this.appending) {
349
402
  this.addPending()
350
- } else if (this !== this.writers.localWriter && !this.isIndexer) {
403
+ } else if (this !== this.writers.localWriter && !this.isIndexer && !this.isBootstrap) {
351
404
  await this.detachAndClose()
352
405
  }
353
406
  }
@@ -367,6 +420,7 @@ class Writer {
367
420
 
368
421
  detachAndClose() {
369
422
  this.detach()
423
+ this.decouple()
370
424
  return this.close()
371
425
  }
372
426
 
@@ -620,7 +674,8 @@ class Writer {
620
674
  batch,
621
675
  views: null,
622
676
  optimistic: optimistic && !this.writers.writable,
623
- value
677
+ value,
678
+ trusted: null
624
679
  }
625
680
 
626
681
  const node = createNode(this.core, this.appendLength++, -1, oplog)
@@ -643,12 +698,16 @@ class Writer {
643
698
  this.processed = 0
644
699
  }
645
700
 
646
- flush(views, queued) {
647
- if (this.pending === null || this.processed === 0) return Promise.resolve()
701
+ async flush(views, queued, trusted) {
702
+ if (this.pending === null || this.processed === 0) return
648
703
 
649
704
  const head = this.pending[this.processed - 1]
650
705
  head.views = views
651
706
 
707
+ head.trusted = trusted
708
+ ? [{ key: trusted.key, length: trusted.length, flushes: views.flushes }]
709
+ : null
710
+
652
711
  if (queued && queued.length) {
653
712
  const attestations = []
654
713
  for (const { key, weight } of queued) {
@@ -668,11 +727,26 @@ class Writer {
668
727
  for (let i = 0; i < this.processed; i++) {
669
728
  buffers.push(encoding.encodeOplog(this.pending[i]))
670
729
  }
671
- this.clear()
672
- return this.core.append(buffers)
730
+
731
+ try {
732
+ await this.core.append(buffers)
733
+ } finally {
734
+ this.clear()
735
+ }
673
736
  }
674
737
  }
675
738
 
739
+ // mirrors topo.cmp - kept local because topo.js requires this module
740
+ function sortsBefore(a, b) {
741
+ if (a.weight !== b.weight) return a.weight > b.weight
742
+ if (a.timestamp !== b.timestamp) return a.timestamp < b.timestamp
743
+
744
+ const c = b4a.compare(a.key, b.key)
745
+ if (c !== 0) return c < 0
746
+
747
+ return a.length < b.length
748
+ }
749
+
676
750
  exports.ActiveWriters = ActiveWriters
677
751
  exports.Writer = Writer
678
752
  exports.createNode = createNode
@@ -692,7 +766,8 @@ function createNode(core, length, weight, oplog) {
692
766
  batch: oplog.batch,
693
767
  views: oplog.views,
694
768
  optimistic: oplog.optimistic,
695
- value: oplog.value
769
+ value: oplog.value,
770
+ trusted: oplog.trusted || []
696
771
  }
697
772
  }
698
773
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.21",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -26,7 +26,7 @@
26
26
  "autobee-encryption": "^0.1.2",
27
27
  "autobee-wakeup": "^1.1.3",
28
28
  "b4a": "^1.8.0",
29
- "compact-encoding": "^2.18.0",
29
+ "compact-encoding": "^3.3.0",
30
30
  "hyperbee2": "^2.10.3",
31
31
  "hypercore": "^11.33.2",
32
32
  "hypercore-id-encoding": "^1.3.0",