autobee 2.0.0-rc.17 → 2.0.0-rc.19

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/index.js CHANGED
@@ -235,6 +235,8 @@ module.exports = class Autobee extends ReadyResource {
235
235
  await this._draining
236
236
  }
237
237
 
238
+ if (this._updating) await this._updating
239
+
238
240
  // let in-flight writer adds finish
239
241
  try {
240
242
  await this._bootingAll
@@ -263,25 +265,6 @@ module.exports = class Autobee extends ReadyResource {
263
265
  if (this.fastForwarding) this.fastForwarding.clearRequests(closingError)
264
266
  }
265
267
 
266
- async changesFrom({ system } = {}) {
267
- const info = await this.system.getInfo()
268
-
269
- const current = {
270
- flushes: info ? info.flushes : 0,
271
- system: this.system.bee.head(),
272
- view: info ? info.view : EMPTY_HEAD
273
- }
274
-
275
- // no common ancestor (null) -> flushes -1 never matches, forcing a full diff
276
- const shared = (await this.system.commonAncestor(system || EMPTY_HEAD)) || {
277
- flushes: -1,
278
- system: EMPTY_HEAD,
279
- view: EMPTY_HEAD
280
- }
281
-
282
- return UpdateChanges.from(shared, current)
283
- }
284
-
285
268
  replicate(...args) {
286
269
  const stream = this.store.replicate(...args)
287
270
  this._wakeup.addStream(stream)
@@ -529,9 +512,6 @@ module.exports = class Autobee extends ReadyResource {
529
512
  else if (head) await this._bootFromHead(head, bootCondition)
530
513
  }
531
514
 
532
- // tracked across drain iteration
533
- this.system.shared = null
534
-
535
515
  this._ackRequired = false
536
516
 
537
517
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
@@ -566,6 +546,8 @@ module.exports = class Autobee extends ReadyResource {
566
546
  }
567
547
 
568
548
  await this._flushLocal()
549
+
550
+ if (!this._interrupting) await this.writers.refresh()
569
551
  } finally {
570
552
  if (this.bumping === 1) this.bumping = 0
571
553
  else this.bumping = 1
@@ -575,16 +557,15 @@ module.exports = class Autobee extends ReadyResource {
575
557
  this._draining = null
576
558
  if (this._interrupting) return
577
559
 
578
- if (this._needsUpdate) {
579
- const updating = rrp()
580
- this._updating = updating.promise
560
+ const updating = rrp()
561
+ this._updating = updating.promise
581
562
 
582
- try {
583
- await this._update(changes)
584
- } finally {
585
- this._updating = null
586
- updating.resolve()
587
- }
563
+ try {
564
+ if (this._needsUpdate) await this._update(changes)
565
+ await this._storeBoot()
566
+ } finally {
567
+ this._updating = null
568
+ updating.resolve()
588
569
  }
589
570
  }
590
571
 
@@ -617,18 +598,28 @@ module.exports = class Autobee extends ReadyResource {
617
598
  }
618
599
  }
619
600
 
620
- async _flushWakeup() {
601
+ // wakeup hints keep arriving asynchronously
602
+ async _applyWakeupHints() {
621
603
  const hints = this._wakeup.flush()
622
- if (!hints.size) return
604
+ if (!hints.size) return hints
623
605
 
624
606
  this.previousDrain = Date.now()
625
607
 
626
608
  for (const [hex, length] of hints) {
627
609
  const key = b4a.from(hex, 'hex')
628
- if (this.writers.has(hex)) continue
610
+ // wakeup() itself no-ops the add for an already-active writer, but still
611
+ // needs to run so it can hint() the announced length - skipping it here
612
+ // for active writers left gc with no hint to protect them
629
613
  await this.writers.wakeup(key, length)
630
614
  }
631
615
 
616
+ return hints
617
+ }
618
+
619
+ async _flushWakeup() {
620
+ const hints = await this._applyWakeupHints()
621
+ if (!hints.size) return
622
+
632
623
  // a scheduled fast-forward is applied by the drain before we look again
633
624
  if (this.fastForwardTo !== null || this.fastForwarding !== null) return
634
625
  if (this._interrupting || this.closing || this.bootFrom) return
@@ -918,41 +909,33 @@ module.exports = class Autobee extends ReadyResource {
918
909
  this._catchupMigratedNodes = null
919
910
  }
920
911
 
921
- let updated = false
922
-
923
- const pending = this.writers.pending.slice()
912
+ // apply the best next node to keep the prefix stable
913
+ const next = await this.writers.nextPendingNode()
914
+ if (next === null) return false
924
915
 
925
- for (let i = pending.length - 1; i >= 0; i--) {
926
- const w = pending[i]
916
+ const { writer: w, batch } = next
927
917
 
928
- const batch = await w.next()
929
- if (batch === null) continue
930
-
931
- if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
932
- await this._processBatch(batch)
933
- w.notify(batch)
934
- updated = true
935
- continue
936
- }
918
+ if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
919
+ await this._processBatch(batch)
920
+ w.notify(batch)
921
+ return true
922
+ }
937
923
 
938
- if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
939
- if (!(await this._optimisticBatch(batch))) {
940
- w.removePending()
941
- continue
942
- }
943
- w.notify(batch)
944
- updated = true
945
- continue
924
+ if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
925
+ if (!(await this._optimisticBatch(batch))) {
926
+ w.removePending()
927
+ return true
946
928
  }
929
+ w.notify(batch)
930
+ return true
947
931
  }
948
932
 
949
- return updated
933
+ return true
950
934
  }
951
935
 
952
936
  async _optimisticBatch(batch) {
953
937
  const rollbackSystem = this.system.bee.head()
954
938
  const rollbackView = this._workingBee.head()
955
- const rollbackShared = this.system.shared
956
939
  const rollbackAttestations = this.writers.attestations.length
957
940
 
958
941
  const t = await this.prepareBatch(batch)
@@ -975,7 +958,6 @@ module.exports = class Autobee extends ReadyResource {
975
958
  this._workingBee.move(rollbackView)
976
959
  this.system.bee.move(rollbackSystem)
977
960
  await this.system.reset()
978
- this.system.shared = rollbackShared
979
961
  // don't attest grants that were just undone
980
962
  this.writers.attestations.length = rollbackAttestations
981
963
  return false
@@ -1064,8 +1046,6 @@ module.exports = class Autobee extends ReadyResource {
1064
1046
 
1065
1047
  this.writers.attest(witnessed)
1066
1048
 
1067
- await this._storeBoot()
1068
-
1069
1049
  for (const { key, added, isAnchor } of changed) {
1070
1050
  if (isAnchor) {
1071
1051
  // no Writer/ActiveWriters tracking, but still wake anything linked to it
@@ -1327,12 +1307,6 @@ module.exports = class Autobee extends ReadyResource {
1327
1307
  this.system.bee.move(head)
1328
1308
  await this.system.reset()
1329
1309
 
1330
- this.system.shared = this.fastForwardTo.shared || {
1331
- flushes: -1,
1332
- view: EMPTY_HEAD,
1333
- system: EMPTY_HEAD
1334
- }
1335
-
1336
1310
  // migrate is set when fast-forwarding from a legacy head
1337
1311
  if (migrate) {
1338
1312
  const view = (await this._handlers.migrate(migrate)) || EMPTY_HEAD
@@ -1345,14 +1319,16 @@ module.exports = class Autobee extends ReadyResource {
1345
1319
  }
1346
1320
 
1347
1321
  this.fastForwardTo = null
1322
+
1323
+ // process any wakeup while fast-forward itself was in flight
1324
+ await this._applyWakeupHints()
1348
1325
  await this.writers.refresh()
1349
1326
 
1350
1327
  // we moved, so ask our peers to tell us their heads again
1351
1328
  this._requestWakeup()
1352
1329
 
1353
- await this._storeBoot()
1354
-
1355
1330
  await this._update(changes)
1331
+ await this._storeBoot()
1356
1332
 
1357
1333
  this.stats.fastForwards++
1358
1334
  this.emit('move-to', to, from)
@@ -261,23 +261,12 @@ module.exports = class FastForward {
261
261
  await Promise.all(promises)
262
262
  if (this.destroyed) return null
263
263
 
264
- const shared = await this._commonAncestor()
265
264
  if (this.destroyed) return null
266
265
 
267
266
  return {
268
267
  head: this.head,
269
268
  tip: this.tip,
270
- migrate: null,
271
- shared
272
- }
273
- }
274
-
275
- async _commonAncestor() {
276
- try {
277
- return await this.auto.system.commonAncestor(this.head)
278
- } catch (err) {
279
- safetyCatch(err)
280
- return null
269
+ migrate: null
281
270
  }
282
271
  }
283
272
 
package/lib/system.js CHANGED
@@ -46,7 +46,6 @@ module.exports = class Systembee {
46
46
  this.indexers = null // legacy
47
47
  this.updates = new Map()
48
48
  this.writers = new Map()
49
- this.shared = null
50
49
  this.encrypted = opts.encrypted === true
51
50
  }
52
51
 
@@ -115,60 +114,6 @@ module.exports = class Systembee {
115
114
  }
116
115
  }
117
116
 
118
- async commonAncestor(head) {
119
- const snap = this.bee.checkout(head)
120
-
121
- const ca = infoIterator(this.bee)
122
- const cb = infoIterator(snap)
123
-
124
- let l = await next(ca)
125
- let r = await next(cb)
126
-
127
- while (l && r) {
128
- if (l.legacy && r.legacy) {
129
- // legacy nodes just compare head
130
- if (sameHead(l.node.head, r.node.head)) {
131
- return {
132
- flushes: l.info.flushes,
133
- system: l.node.head,
134
- view: l.info.view || EMPTY_HEAD
135
- }
136
- }
137
-
138
- // different legacy cores never converge - genuinely unrelated history
139
- if (!b4a.equals(l.node.head.key, r.node.head.key)) return null
140
-
141
- if (l.node.head.length >= r.node.head.length) l = await next(ca)
142
- else r = await next(cb)
143
- continue
144
- }
145
-
146
- if (l.legacy !== r.legacy) {
147
- // current-format history sits above the legacy band: descend it
148
- if (!l.legacy) l = await next(ca)
149
- else r = await next(cb)
150
- continue
151
- }
152
-
153
- if (l.info.flushes === r.info.flushes && sameHead(l.info.view, r.info.view)) {
154
- return {
155
- flushes: l.info.flushes,
156
- system: l.node.head,
157
- view: l.info.view || EMPTY_HEAD
158
- }
159
- }
160
-
161
- const lf = l.info.flushes
162
- const rf = r.info.flushes
163
-
164
- if (lf >= rf) l = await next(ca)
165
- if (rf >= lf) r = await next(cb)
166
- }
167
-
168
- // no common ancestor
169
- return null
170
- }
171
-
172
117
  async reset({ timeout } = {}) {
173
118
  const info = await this.getInfo({ timeout })
174
119
 
@@ -178,7 +123,6 @@ module.exports = class Systembee {
178
123
  this.timestamp = info ? info.timestamp : 0
179
124
  this.flushes = info ? info.flushes : 0
180
125
  this.indexers = info ? info.indexers : null
181
- this.shared = null
182
126
  this.updates.clear()
183
127
  this.writers.clear()
184
128
 
@@ -200,16 +144,8 @@ module.exports = class Systembee {
200
144
  async undo(head) {
201
145
  this.bee.move(head)
202
146
 
203
- const shared = this.shared
204
147
  await this.reset()
205
148
 
206
- // deepest rewind since last mark - everything at or below it is untouched
207
- if (shared === null || this.flushes < shared.flushes) {
208
- this.shared = { flushes: this.flushes, view: this.view || EMPTY_HEAD, system: head }
209
- } else if (shared) {
210
- this.shared = shared
211
- }
212
-
213
149
  return this.view || EMPTY_HEAD
214
150
  }
215
151
 
@@ -514,41 +450,3 @@ function getInfo(data) {
514
450
 
515
451
  return null
516
452
  }
517
-
518
- function sameHead(a, b) {
519
- if (!a || !b || a.length !== b.length) return false
520
- if (!a.key || !b.key) return a.key === b.key
521
- return b4a.equals(a.key, b.key)
522
- }
523
-
524
- async function* infoIterator(bee) {
525
- try {
526
- for await (const node of bee.createChangesStream()) {
527
- const legacy = topo.isLegacyNode(node)
528
- const info = legacy ? getLegacyInfo(node) : getInfo(node)
529
- if (info) yield { node, info, legacy }
530
- }
531
- } catch {
532
- // fall through
533
- }
534
- }
535
-
536
- async function next(it) {
537
- const res = await it.next()
538
- return res.done ? null : res.value
539
- }
540
-
541
- function getLegacyInfo(data) {
542
- for (const { keys } of data.batch) {
543
- for (const k of keys) {
544
- if (b4a.equals(k.key, INFO_LEGACY_KEY)) {
545
- try {
546
- return encoding.decodeSystemInfo(k.value)
547
- } catch {
548
- return null
549
- }
550
- }
551
- }
552
- }
553
- return null
554
- }
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
@@ -229,6 +231,8 @@ function cmp(a, b) {
229
231
  }
230
232
 
231
233
  async function inflate(auto, reorder, opts = null) {
234
+ const readOpts = opts === null ? { timeout: DEFAULT_OP_TIMEOUT } : opts
235
+
232
236
  const promises = []
233
237
  const sessions = new Map()
234
238
 
@@ -257,7 +261,7 @@ async function inflate(auto, reorder, opts = null) {
257
261
  sessions.set(id, core)
258
262
  }
259
263
 
260
- promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, opts))
264
+ promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, readOpts))
261
265
  }
262
266
 
263
267
  try {
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,13 +64,15 @@ 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
 
70
+ // remember the announced length so gc doesn't drop a writer we haven't caught up on
71
+ w.hint(length)
72
+
67
73
  // prefetch the oplog head for the fast-forward that runs right after us
68
74
  if (w && length > 0) w.core.get(length - 1).catch(noop)
69
75
 
70
- // TODO: signal that we've seen length on w for gc
71
-
72
76
  this.bump()
73
77
  }
74
78
 
@@ -113,6 +117,9 @@ class ActiveWriters {
113
117
  }
114
118
 
115
119
  async refresh() {
120
+ // peeked batches were read against the pre-reboot system, drop them
121
+ this._pendingBuffer.clear()
122
+
116
123
  if (this.localWriter && !this.localWriter.isClosed) {
117
124
  await this.updateLocalState()
118
125
  }
@@ -127,6 +134,33 @@ class ActiveWriters {
127
134
  await this.localWriter.update()
128
135
  }
129
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
+
130
164
  _updateLocalState() {
131
165
  const w = this.localWriter
132
166
 
@@ -215,6 +249,7 @@ class ActiveWriters {
215
249
  const id = b4a.toString(bootstrap.key, 'hex')
216
250
  let w = this.active.get(id)
217
251
  if (w) {
252
+ w.isBootstrap = true
218
253
  w.couple() // couple if we weren't ready first time
219
254
  return
220
255
  }
@@ -223,6 +258,7 @@ class ActiveWriters {
223
258
  session.setEncryption(this.auto._getEncryptionProvider())
224
259
 
225
260
  w = new Writer(this, this.auto.system, false, session, id)
261
+ w.isBootstrap = true // the root of the autobase, never gc'd
226
262
  w.attach()
227
263
  w.couple()
228
264
  }
@@ -253,6 +289,7 @@ class Writer {
253
289
  this.appendLength = this.core.length + 1
254
290
  this.waiting = null
255
291
  this.download = null
292
+ this.hintedLength = 0
256
293
 
257
294
  this.isPending = false
258
295
  this.isClosed = false
@@ -261,6 +298,7 @@ class Writer {
261
298
  this.isRemoved = false
262
299
  this.isCoupled = false
263
300
  this.isFrozen = false
301
+ this.isBootstrap = false
264
302
 
265
303
  this._onchangeBound = this._onchange.bind(this)
266
304
 
@@ -308,6 +346,8 @@ class Writer {
308
346
  if (!this.isPending) return
309
347
  this.isPending = false
310
348
 
349
+ this.writers._pendingBuffer.delete(this)
350
+
311
351
  const head = this.writers.pending.pop()
312
352
  if (head !== this) {
313
353
  this.writers.pending[this.index] = head
@@ -315,6 +355,11 @@ class Writer {
315
355
  }
316
356
  }
317
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
+
318
363
  _onchange() {
319
364
  if (this.waiting === null) this.bump()
320
365
  }
@@ -352,10 +397,10 @@ class Writer {
352
397
 
353
398
  const length = await this.update()
354
399
 
355
- // don't drop any local nodes left in buffer
356
- 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) {
357
402
  this.addPending()
358
- } else if (this !== this.writers.localWriter && !this.isIndexer) {
403
+ } else if (this !== this.writers.localWriter && !this.isIndexer && !this.isBootstrap) {
359
404
  await this.detachAndClose()
360
405
  }
361
406
  }
@@ -375,6 +420,7 @@ class Writer {
375
420
 
376
421
  detachAndClose() {
377
422
  this.detach()
423
+ this.decouple()
378
424
  return this.close()
379
425
  }
380
426
 
@@ -690,6 +736,17 @@ class Writer {
690
736
  }
691
737
  }
692
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
+
693
750
  exports.ActiveWriters = ActiveWriters
694
751
  exports.Writer = Writer
695
752
  exports.createNode = createNode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.17",
3
+ "version": "2.0.0-rc.19",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",