autobee 2.0.0-rc.9 → 2.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/lib/writers.js CHANGED
@@ -1,7 +1,8 @@
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 { LEGACY_OPLOG_VERSION } = require('./constants.js')
5
+ const { resolveWeight } = require('./witness.js')
5
6
  const { WriterEncryption } = require('autobee-encryption')
6
7
 
7
8
  const WRITER_PREFETCH = 10
@@ -17,17 +18,20 @@ class ActiveWriters {
17
18
 
18
19
  this.auto = auto
19
20
  this.active = new Map()
20
- this._adding = new Map()
21
21
  this.triggers = new Triggers()
22
- this.localWriter = new Writer(this, auto.system, true, local, b4a.toString(local.key, 'hex'))
22
+ this.localWriter = null
23
23
  this.writable = false
24
24
  this.pending = []
25
- this.witnesses = []
26
- this.attestations = []
27
25
 
26
+ this._pendingBuffer = new Map()
27
+ this._adding = new Map()
28
+
29
+ this.localWriter = new Writer(this, auto.system, true, local, b4a.toString(local.key, 'hex'))
28
30
  this.localWriter.attach()
31
+ }
29
32
 
30
- this._bootstrap()
33
+ get size() {
34
+ return this.active.size
31
35
  }
32
36
 
33
37
  [Symbol.iterator]() {
@@ -57,24 +61,31 @@ class ActiveWriters {
57
61
  let w = this.active.get(id)
58
62
 
59
63
  if (w === this.localWriter) return
64
+
65
+ let added = false
60
66
  if (!w) {
61
- // anchors are local-only and never replicated - nothing to fetch
62
- const info = await this.auto.system.get(key, { unflushed: true })
63
- if (info && info.isAnchor) return
64
67
  w = await this.add(key)
68
+ if (!w) return
69
+ added = true
65
70
  }
66
71
 
67
- // TODO: signal that we've seen length on w for gc
72
+ const advanced = length > w.hintedLength
73
+
74
+ // remember the announced length so gc doesn't drop a writer we haven't caught up on
75
+ w.hint(length)
76
+
77
+ // prefetch the oplog head for the fast-forward that runs right after us
78
+ if (w && length > 0) w.core.get(length - 1).catch(noop)
68
79
 
69
- this.bump()
80
+ if (added || advanced) this.bump()
70
81
  }
71
82
 
72
83
  getLatestLocalOplog() {
73
84
  return this.localWriter.getLatest()
74
85
  }
75
86
 
76
- appendLocal(value, timestamp, batch, links, optimistic, witness) {
77
- return this.localWriter.append(value, timestamp, batch, links, optimistic, witness)
87
+ appendLocal(value, timestamp, batch, links, optimistic, witness, approvals = null) {
88
+ return this.localWriter.append(value, timestamp, batch, links, optimistic, witness, approvals)
78
89
  }
79
90
 
80
91
  async rotateLocalWriter(core) {
@@ -85,9 +96,6 @@ class ActiveWriters {
85
96
  this.localWriter.addPending()
86
97
  this.auto.emit('writer', this.localWriter)
87
98
 
88
- // old local key witnesses are useless
89
- this.witnesses = []
90
-
91
99
  await this.updateLocalState()
92
100
  }
93
101
 
@@ -95,19 +103,21 @@ class ActiveWriters {
95
103
  return this.localWriter.clear()
96
104
  }
97
105
 
98
- flushLocal(views) {
106
+ async flushLocal(views) {
99
107
  const w = this.localWriter
100
108
 
101
109
  // only consume the queue when a flush will actually write
102
- if (w.pending === null || w.processed === 0) return Promise.resolve()
110
+ if (w.pending === null || w.processed === 0) return
103
111
 
104
- const queued = this.attestations
105
- this.attestations = []
112
+ const trusted = await this.auto.trusted.mostRecentTrusted(this.auto._workingView.view, null)
106
113
 
107
- return w.flush(views, queued)
114
+ return w.flush(views, trusted)
108
115
  }
109
116
 
110
117
  async refresh() {
118
+ // peeked batches were read against the pre-reboot system, drop them
119
+ this._pendingBuffer.clear()
120
+
111
121
  if (this.localWriter && !this.localWriter.isClosed) {
112
122
  await this.updateLocalState()
113
123
  }
@@ -122,6 +132,37 @@ class ActiveWriters {
122
132
  await this.localWriter.update()
123
133
  }
124
134
 
135
+ async nextPendingNode({ local = false } = {}) {
136
+ for (const w of this.pending.slice()) {
137
+ if (local && w !== this.localWriter) continue
138
+ if (this._pendingBuffer.has(w) || w.isFrozen) continue
139
+
140
+ const batch = await w.next()
141
+ if (batch !== null) this._pendingBuffer.set(w, batch)
142
+ }
143
+
144
+ if (this._pendingBuffer.size === 0) return null
145
+
146
+ let writer = null
147
+ let best = null
148
+
149
+ for (const [w, batch] of this._pendingBuffer) {
150
+ if (local && w !== this.localWriter) continue
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
+ if (writer === null) return null
161
+
162
+ this._pendingBuffer.delete(writer)
163
+ return { writer, batch: best }
164
+ }
165
+
125
166
  _updateLocalState() {
126
167
  const w = this.localWriter
127
168
 
@@ -157,10 +198,7 @@ class ActiveWriters {
157
198
  if (local) await this.updateLocalState()
158
199
 
159
200
  let w = this.active.get(id)
160
- if (w) {
161
- w.couple() // couple if we weren't ready first time
162
- return w
163
- }
201
+ if (w) return w
164
202
 
165
203
  // no new sessions once teardown started
166
204
  if (this.auto._interrupting) return null
@@ -168,6 +206,7 @@ class ActiveWriters {
168
206
  const encryption = this.auto.encryptionKey ? new WriterEncryption(this.auto) : null
169
207
 
170
208
  const core = this.auto.store.get({
209
+ active: false,
171
210
  key,
172
211
  encryption,
173
212
  group: local || !this.auto.wakeupCapability ? null : this.auto.wakeupCapability.discoveryKey
@@ -184,7 +223,6 @@ class ActiveWriters {
184
223
 
185
224
  w = new Writer(this, this.auto.system, false, core, id)
186
225
  w.attach()
187
- w.couple()
188
226
 
189
227
  return w
190
228
  }
@@ -196,42 +234,22 @@ class ActiveWriters {
196
234
  const w = this.active.get(id)
197
235
  if (!w) return
198
236
 
237
+ // nodes this writer appended before the removal may still sort before it,
238
+ // and their ordering advances its record everywhere - closing the session
239
+ // while we are behind strands us one node short forever. reset() closes it
240
+ // once we have caught up
241
+ if (w !== this.localWriter) {
242
+ const length = await w.update()
243
+ if (length < w.core.length || length < w.hintedLength) {
244
+ w.addPending()
245
+ return
246
+ }
247
+ }
248
+
199
249
  w.detach()
200
250
  w.decouple()
201
251
  if (w !== this.localWriter) await w.close()
202
252
  }
203
-
204
- _bootstrap() {
205
- const bootstrap = this.auto.bootstrap
206
-
207
- bootstrap.setEncryption(this.auto._getEncryptionProvider())
208
- bootstrap.setActive(true)
209
-
210
- const id = b4a.toString(bootstrap.key, 'hex')
211
- let w = this.active.get(id)
212
- if (w) {
213
- w.couple() // couple if we weren't ready first time
214
- return
215
- }
216
-
217
- const session = bootstrap.session()
218
- session.setEncryption(this.auto._getEncryptionProvider())
219
-
220
- w = new Writer(this, this.auto.system, false, session, id)
221
- w.attach()
222
- w.couple()
223
- }
224
-
225
- attest(witnessed) {
226
- const local = this.localWriter
227
-
228
- for (const { key, weight } of witnessed) {
229
- if (b4a.equals(key, local.core.key)) continue // never attest about myself
230
- if (this.attestations.some((a) => a.weight === weight && b4a.equals(a.key, key))) continue
231
-
232
- this.attestations.push({ key, weight })
233
- }
234
- }
235
253
  }
236
254
 
237
255
  class Writer {
@@ -248,6 +266,7 @@ class Writer {
248
266
  this.appendLength = this.core.length + 1
249
267
  this.waiting = null
250
268
  this.download = null
269
+ this.hintedLength = 0
251
270
 
252
271
  this.isPending = false
253
272
  this.isClosed = false
@@ -256,17 +275,26 @@ class Writer {
256
275
  this.isRemoved = false
257
276
  this.isCoupled = false
258
277
  this.isFrozen = false
278
+ this.isHead = false
259
279
 
260
280
  this._onchangeBound = this._onchange.bind(this)
261
281
 
262
282
  if (!local) this.core.on('append', this._onchangeBound)
263
283
  if (!local) this.core.on('download', this._onchangeBound)
264
-
265
- this.couple()
266
284
  }
267
285
 
268
286
  get isIndexer() {
269
- return this.weight === 2
287
+ return this.weight >= 2
288
+ }
289
+
290
+ isSystemHead() {
291
+ if (this.core.length === 0) return false
292
+
293
+ if (this.system.heads.length === 0) {
294
+ return b4a.equals(this.core.key, this.writers.auto.key)
295
+ }
296
+
297
+ return this.system.isHead(this.core.key)
270
298
  }
271
299
 
272
300
  couple() {
@@ -303,6 +331,8 @@ class Writer {
303
331
  if (!this.isPending) return
304
332
  this.isPending = false
305
333
 
334
+ this.writers._pendingBuffer.delete(this)
335
+
306
336
  const head = this.writers.pending.pop()
307
337
  if (head !== this) {
308
338
  this.writers.pending[this.index] = head
@@ -310,7 +340,16 @@ class Writer {
310
340
  }
311
341
  }
312
342
 
343
+ // a peer announced this length for us, so we're not caught up until we've seen it
344
+ hint(length) {
345
+ if (length > this.hintedLength) {
346
+ this.hintedLength = length
347
+ this.core.setActive(true)
348
+ }
349
+ }
350
+
313
351
  _onchange() {
352
+ this.hint(this.core.length)
314
353
  if (this.waiting === null) this.bump()
315
354
  }
316
355
 
@@ -347,10 +386,10 @@ class Writer {
347
386
 
348
387
  const length = await this.update()
349
388
 
350
- // don't drop any local nodes left in buffer
351
- if (length < this.core.length || this.appending) {
389
+ // don't drop any local nodes left in buffer, or work a peer told us about
390
+ if (length < this.core.length || length < this.hintedLength || this.appending) {
352
391
  this.addPending()
353
- } else if (this !== this.writers.localWriter && !this.isIndexer) {
392
+ } else if (this !== this.writers.localWriter && !this.isHead) {
354
393
  await this.detachAndClose()
355
394
  }
356
395
  }
@@ -370,6 +409,7 @@ class Writer {
370
409
 
371
410
  detachAndClose() {
372
411
  this.detach()
412
+ this.decouple()
373
413
  return this.close()
374
414
  }
375
415
 
@@ -396,10 +436,24 @@ class Writer {
396
436
 
397
437
  async update() {
398
438
  const info = await this.system.get(this.core.key)
439
+ const length = info ? info.length : 0
399
440
 
400
- if (info && info.isAnchor && this !== this.writers.localWriter) {
401
- await this.detachAndClose()
402
- return info.length
441
+ const isHead = this.isSystemHead()
442
+
443
+ if (isHead) {
444
+ this.isHead = true
445
+ this.couple()
446
+ } else {
447
+ this.isHead = false
448
+ this.decouple()
449
+ }
450
+
451
+ const active = length < this.core.length || length < this.hintedLength
452
+
453
+ if (this.isHead || active) {
454
+ this.core.setActive(true)
455
+ } else {
456
+ this.core.setActive(false)
403
457
  }
404
458
 
405
459
  if (!info) {
@@ -415,7 +469,7 @@ class Writer {
415
469
  this.writers._updateLocalState()
416
470
  }
417
471
 
418
- return info ? info.length : 0
472
+ return length
419
473
  }
420
474
 
421
475
  next() {
@@ -493,30 +547,8 @@ class Writer {
493
547
  return null
494
548
  }
495
549
 
496
- for (const att of node.attestations || []) {
497
- if (!b4a.equals(att.key, this.writers.localWriter.core.key)) continue
498
-
499
- const seen = this.writers.witnesses.some(
500
- (w) =>
501
- w.length === node.length &&
502
- w.attestation.weight === att.weight &&
503
- b4a.equals(w.key, node.key)
504
- )
505
- if (seen) continue
506
-
507
- const manifest = this.core.getManifest({ raw: true })
508
- if (!manifest) continue
509
-
510
- this.writers.witnesses.push({
511
- key: node.key,
512
- length: node.length,
513
- attestation: att,
514
- manifest
515
- })
516
- }
517
-
518
550
  batch.push(node)
519
- if (b.end === 0) return batch
551
+ if (b.end === 0) return finalizeNodeBatch(batch)
520
552
 
521
553
  // TODO: mark as dead
522
554
  if (batch.length >= BATCH_WRITER_SANITY) return null
@@ -572,17 +604,24 @@ class Writer {
572
604
  }
573
605
 
574
606
  async _isLinked(node) {
575
- // the backer gates like a link: present-or-wait
576
607
  let links = node.links
577
608
  if (node.witness) {
578
609
  links = links.slice()
579
- links.push({ key: node.witness.backer.key, length: node.witness.backer.length })
610
+ links.push({ key: node.witness.link.key, length: node.witness.link.length })
580
611
  }
581
612
 
582
613
  const promises = new Array(links.length)
583
614
 
584
615
  for (let i = 0; i < links.length; i++) {
585
- promises[i] = this.system.has(links[i])
616
+ const link = links[i]
617
+
618
+ // legacy batch nodes link their own previous node, satisfied by oplog order
619
+ if (link.length < node.length && b4a.equals(link.key, this.core.key)) {
620
+ promises[i] = true
621
+ continue
622
+ }
623
+
624
+ promises[i] = this.system.has(link)
586
625
  }
587
626
 
588
627
  const has = await Promise.all(promises)
@@ -612,16 +651,18 @@ class Writer {
612
651
  return this.pending !== null
613
652
  }
614
653
 
615
- append(value, timestamp, batch, links, optimistic, witness) {
654
+ append(value, timestamp, batch, links, optimistic, witness, approvals = null) {
616
655
  if (this.pending === null) this.pending = []
617
656
 
618
657
  const oplog = {
619
658
  version: encoding.OPLOG_VERSION,
620
659
  timestamp,
621
660
  links,
622
- witness: witness || null,
623
661
  batch,
624
662
  views: null,
663
+ trusted: null,
664
+ witness: witness || null,
665
+ approvals,
625
666
  optimistic: optimistic && !this.writers.writable,
626
667
  value
627
668
  }
@@ -646,39 +687,43 @@ class Writer {
646
687
  this.processed = 0
647
688
  }
648
689
 
649
- flush(views, queued) {
650
- if (this.pending === null || this.processed === 0) return Promise.resolve()
690
+ async flush(views, trusted) {
691
+ if (this.pending === null || this.processed === 0) return
651
692
 
652
693
  const head = this.pending[this.processed - 1]
653
694
  head.views = views
654
695
 
655
- if (queued && queued.length) {
656
- const attestations = []
657
- for (const { key, weight } of queued) {
658
- const signature = signAttestation(
659
- this.core.keyPair.secretKey,
660
- this.core.key,
661
- key,
662
- head.length,
663
- weight
664
- )
665
- attestations.push({ key, weight, signature })
666
- }
667
- head.attestations = attestations
668
- }
696
+ head.trusted = trusted ? [{ key: trusted.key, length: trusted.length }] : null
697
+ this.writers.auto.emit('appending', this.pending)
669
698
 
670
699
  const buffers = []
671
700
  for (let i = 0; i < this.processed; i++) {
672
701
  buffers.push(encoding.encodeOplog(this.pending[i]))
673
702
  }
674
- this.clear()
675
- return this.core.append(buffers)
703
+
704
+ try {
705
+ await this.core.append(buffers)
706
+ } finally {
707
+ this.clear()
708
+ }
676
709
  }
677
710
  }
678
711
 
712
+ // mirrors topo.cmp - kept local because topo.js requires this module
713
+ function sortsBefore(a, b) {
714
+ if (a.weight !== b.weight) return a.weight > b.weight
715
+ if (a.timestamp !== b.timestamp) return a.timestamp < b.timestamp
716
+
717
+ const c = b4a.compare(a.key, b.key)
718
+ if (c !== 0) return c < 0
719
+
720
+ return a.length < b.length
721
+ }
722
+
679
723
  exports.ActiveWriters = ActiveWriters
680
724
  exports.Writer = Writer
681
725
  exports.createNode = createNode
726
+ exports.finalizeNodeBatch = finalizeNodeBatch
682
727
 
683
728
  function createNode(core, length, weight, oplog) {
684
729
  return {
@@ -690,13 +735,32 @@ function createNode(core, length, weight, oplog) {
690
735
  version: oplog.version,
691
736
  timestamp: oplog.timestamp,
692
737
  links: oplog.links,
693
- witness: oplog.witness || null,
694
- attestations: oplog.attestations || null,
695
738
  batch: oplog.batch,
696
739
  views: oplog.views,
740
+ trusted: oplog.trusted || [],
741
+ witness: oplog.witness || null,
742
+ approvals: oplog.approvals || null,
697
743
  optimistic: oplog.optimistic,
698
744
  value: oplog.value
699
745
  }
700
746
  }
701
747
 
748
+ function finalizeNodeBatch(batch) {
749
+ // replay batches keep unreachable blocks as null holes (topo.getOplogBatch)
750
+ let first = null
751
+ for (const node of batch) {
752
+ if (node !== null) {
753
+ first = node
754
+ break
755
+ }
756
+ }
757
+ if (first === null || first.version > LEGACY_OPLOG_VERSION) return batch
758
+
759
+ for (let i = 0; i < batch.length; i++) {
760
+ if (batch[i] !== null) batch[i].batch.start = i
761
+ }
762
+
763
+ return batch
764
+ }
765
+
702
766
  function noop() {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.9",
3
+ "version": "2.0.0",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -38,11 +38,12 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "brittle": "^4.1.0",
41
- "corestore": "^7.11.0",
41
+ "corestore": "^7.12.2",
42
42
  "hypercore-crypto": "^3.6.1",
43
43
  "lunte": "^1.6.0",
44
44
  "prettier": "^3.8.1",
45
45
  "prettier-config-holepunch": "^2.0.0",
46
- "protomux-wakeup": "^2.9.0"
46
+ "protomux-wakeup": "^2.9.0",
47
+ "replication-simulator": "^3.0.0"
47
48
  }
48
49
  }