autobee 2.2.2 → 2.3.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
@@ -247,17 +247,16 @@ Called at flush time to stamp your own oplog (with your view as `target` and a `
247
247
 
248
248
  ```js
249
249
  {
250
- head: { key, length }, // oplog head to boot from
251
- legacy: { key, length }, // pre-2.0 pointer, see below
252
- bootCondition (target, reference) {} // optional gate on the view we would land on
250
+ head: { key, length }, // oplog head to boot from
251
+ legacy: { key, length } // pre-2.0 pointer, see below
253
252
  }
254
253
  ```
255
254
 
256
255
  Pass one of `head` or `legacy`, not both.
257
256
 
258
- Without `bootCondition` this is a single attempt that gives up if the head cannot be read; with one it parks until the condition is satisfied, which today means indefinitely if it never is.
257
+ `head` must be a v3 or newer oplog node. `length` is a floor: the key is searched for its latest oplog head at or past it, and the boot lands ungated on the views that head stamps. It is a single attempt: use `moveTo` if you need to retry.
259
258
 
260
- `legacy` is for records written before boot heads were oplog heads: the key is a _system_ head and a `0` length is resolved from the core. It boots ungated - `bootCondition` does not apply - and will be removed, so don't reach for it. A bare `{ key, length }` in place of the whole struct means the same thing, older still.
259
+ `legacy` is the pre-2.0 pointer: a _system_ head whose `0` length resolves from the core. It will be removed, so don't reach for it. A bare `{ key, length }` in place of the whole struct means the same.
261
260
 
262
261
  #### `fastForward.conservative`
263
262
 
@@ -265,9 +264,11 @@ Defaults to `true`: only fast-forward onto a head a connected peer can serve who
265
264
 
266
265
  The check covers the oplog head only, not the system and view cores the fast-forward then reads.
267
266
 
268
- #### `await db.moveTo(head)`
267
+ #### `await db.moveTo(head, [options])`
269
268
 
270
- Fast-forward onto an oplog head, ignoring the usual distance and conservative checks. Resolves `{ to, from }`.
269
+ Fast-forward onto an oplog head, ignoring the usual distance and conservative checks. Resolves `{ to, from }`, or `null` if the head could not be booted.
270
+
271
+ Pass `{ timeout }` to bound the reads, so a head nobody can serve fails instead of hanging.
271
272
 
272
273
  ### Encryption
273
274
 
package/index.js CHANGED
@@ -14,7 +14,6 @@ const asserts = require('./lib/asserts.js')
14
14
  const boot = require('./lib/boot.js')
15
15
  const { resolveWeight, currentWeight } = require('./lib/witness.js')
16
16
  const encoding = require('./lib/encoding.js')
17
- const { LEGACY_OPLOG_VERSION } = require('./lib/constants.js')
18
17
  const FastForward = require('./lib/fast-forward.js')
19
18
  const System = require('./lib/system.js')
20
19
  const ApplyCalls = require('./lib/apply-calls.js')
@@ -72,7 +71,7 @@ module.exports = class Autobee extends ReadyResource {
72
71
  this.stats = { undos: 0, fastForwards: 0, drains: 0, applies: 0, appends: 0 }
73
72
 
74
73
  const systemStore = this.store.session()
75
- this.system = new System(systemStore, this.name, {
74
+ this.system = new System(this, systemStore, {
76
75
  core: systemStore.get({ preload: this._getCorePreload('system') }),
77
76
  encrypted: this.encrypted,
78
77
  getEncryptionProvider: this.getSystemEncryption
@@ -119,7 +118,6 @@ module.exports = class Autobee extends ReadyResource {
119
118
  this._appending = []
120
119
  this._draining = null
121
120
  this._updating = null
122
- this._bootWait = null
123
121
 
124
122
  this.legacyViews = handlers.legacyViews || []
125
123
 
@@ -145,7 +143,6 @@ module.exports = class Autobee extends ReadyResource {
145
143
  this.interrupted = null
146
144
  this._interrupting = false
147
145
  this._onErrorBound = this._onError.bind(this)
148
- this._bumpSoonBound = this.bumpSoon.bind(this)
149
146
  this._onGroupUpdateBound = this._onGroupUpdate.bind(this)
150
147
 
151
148
  this.wakeupCapability = null
@@ -153,7 +150,7 @@ module.exports = class Autobee extends ReadyResource {
153
150
  this.previousDrain = 0
154
151
 
155
152
  this._catchupMigratedNodes = null
156
- this._migratedHead = null
153
+ this._migrating = false
157
154
  this._prebooting = null
158
155
 
159
156
  this.ready().catch(noop)
@@ -393,7 +390,6 @@ module.exports = class Autobee extends ReadyResource {
393
390
  // rugpull the rest
394
391
  await this.store.close()
395
392
 
396
- if (this._bootWait !== null) this._bootWait.resolve()
397
393
  if (this._updating) await this._updating
398
394
  if (this._draining) await this._draining
399
395
 
@@ -437,10 +433,6 @@ module.exports = class Autobee extends ReadyResource {
437
433
  return new WriterEncryption(this)
438
434
  }
439
435
 
440
- getMostRecentHead() {
441
- return topo.getMostRecentHead(this, this.system.bee.snapshot())
442
- }
443
-
444
436
  async _preBoot() {
445
437
  if (this._handlers.wait) await this._handlers.wait()
446
438
 
@@ -458,7 +450,7 @@ module.exports = class Autobee extends ReadyResource {
458
450
  this.bootFrom = getBootOption(await this.bootFrom)
459
451
  }
460
452
 
461
- return boot(this.store, this.key, this.legacyViews, {
453
+ return boot(this.store, this.key, {
462
454
  encryptionKey: this.encryptionKey,
463
455
  keyPair: this.keyPair
464
456
  })
@@ -505,37 +497,15 @@ module.exports = class Autobee extends ReadyResource {
505
497
  const system = result.system || EMPTY_HEAD
506
498
 
507
499
  await this.system.boot(system)
500
+ if (this.system.migration) await this._runMigration()
508
501
 
509
- const migrated = await this.local.getUserData('autobee/migrated-head')
510
- if (migrated) this._migratedHead = encoding.decodeMigratedHead(migrated)
511
-
512
- let view = this.system.view
513
- if (!view) view = (this._migratedHead && this._migratedHead.view) || EMPTY_HEAD
514
-
515
- // @todo migration
516
502
  if (result.migration) {
517
- if (this._handlers.migrate) {
518
- view =
519
- (await this._handlers.migrate(result.migration.views, result.migration.system)) ||
520
- EMPTY_HEAD
521
- this._catchupMigratedNodes = result.migration.catchup
522
-
523
- this._migratedHead = {
524
- system: result.migration.system,
525
- view: view.length ? view : (this._migratedHead && this._migratedHead.view) || null
526
- }
527
-
528
- await this._storeMigratedHead()
529
- }
503
+ this._migrating = true
504
+ this._catchupMigratedNodes = result.migration.catchup
530
505
 
531
506
  // ff boot invalidated by migration
532
507
  this.bootFrom = null
533
508
 
534
- // clear legacy data
535
- await this.bootstrap.setUserData('autobase/local', null)
536
- await this.local.setUserData('autobase/boot', null)
537
- await this.local.setUserData('autobase/encryption', null)
538
-
539
509
  for (const batch of result.migration.catchup) {
540
510
  const { key, length } = batch[batch.length - 1]
541
511
  this.writers.wakeup(key, length)
@@ -547,6 +517,8 @@ module.exports = class Autobee extends ReadyResource {
547
517
  await this._workingBee.core.ready()
548
518
  await this.bee.core.ready()
549
519
 
520
+ const view = this.system.view
521
+
550
522
  this._workingBee.move(view)
551
523
  this.bee.move(view)
552
524
 
@@ -592,10 +564,6 @@ module.exports = class Autobee extends ReadyResource {
592
564
  async _bump(force) {
593
565
  if (!force && !this._bootGuard.opened) await this._bootGuard.ready()
594
566
 
595
- // resolve before the bootAll gate: a parked boot-from retries on any bump,
596
- // and its drain is the one _bootOnline itself is waiting on
597
- if (this._bootWait !== null) this._bootWait.resolve()
598
-
599
567
  if (!force && !this._bootOnlineGuard.opened) await this._bootOnlineGuard.ready()
600
568
 
601
569
  this.bumping++
@@ -668,12 +636,16 @@ module.exports = class Autobee extends ReadyResource {
668
636
  this.stats.drains++
669
637
 
670
638
  if (this.bootFrom) {
671
- const { head = null, legacy = null, bootCondition = null, wait = false } = this.bootFrom
639
+ const { head = null, legacy = null } = this.bootFrom
672
640
 
673
641
  this.bootFrom = null
674
642
 
675
- if (legacy) await this._bootFromSystem(legacy)
676
- else if (head) await this._bootFromHead(head, bootCondition, wait)
643
+ if (legacy) {
644
+ await this._bootFromSystem(legacy)
645
+ } else if (head) {
646
+ this._wakeup.hint({ key: head.key, length: head.length || 0 })
647
+ await this._bootFromHead(head)
648
+ }
677
649
  }
678
650
 
679
651
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
@@ -695,6 +667,8 @@ module.exports = class Autobee extends ReadyResource {
695
667
 
696
668
  if (this.fastForwardTo !== null) {
697
669
  await this._applyFastForward()
670
+ if (changes) changes.track()
671
+ this._needsUpdate = false
698
672
  break // revaluate conditions...
699
673
  }
700
674
 
@@ -718,7 +692,7 @@ module.exports = class Autobee extends ReadyResource {
718
692
  this._updating = updating.promise
719
693
 
720
694
  try {
721
- if (this._needsUpdate) await this._update(changes)
695
+ if (this._needsUpdate) await this._update(changes, false)
722
696
  await this._storeBoot()
723
697
  } finally {
724
698
  this._updating = null
@@ -900,13 +874,13 @@ module.exports = class Autobee extends ReadyResource {
900
874
  }
901
875
  }
902
876
 
903
- async _update(changes) {
877
+ async _update(changes, fastForward) {
904
878
  this._needsUpdate = false
905
879
  this.bee.update(this._workingBee.root)
906
880
 
907
881
  if (!changes) return
908
882
 
909
- changes.finalise()
883
+ changes.finalise(fastForward)
910
884
  await this._handlers.update(this.view, changes)
911
885
  }
912
886
 
@@ -1205,9 +1179,6 @@ module.exports = class Autobee extends ReadyResource {
1205
1179
  this.stats.undos++
1206
1180
  this.trusted.clear()
1207
1181
  t.view = await this.system.undo(t.undo)
1208
- if (!t.view.length && this._migratedHead && this._migratedHead.view) {
1209
- t.view = this._migratedHead.view
1210
- }
1211
1182
  }
1212
1183
 
1213
1184
  return t
@@ -1288,8 +1259,9 @@ module.exports = class Autobee extends ReadyResource {
1288
1259
  }
1289
1260
  }
1290
1261
 
1291
- _storeBoot() {
1262
+ async _storeBoot() {
1292
1263
  const proms = []
1264
+
1293
1265
  proms.push(
1294
1266
  this.local.setUserData(
1295
1267
  'autobee/previous-drain',
@@ -1302,12 +1274,15 @@ module.exports = class Autobee extends ReadyResource {
1302
1274
  proms.push(this.local.setUserData('autobee/head', encoding.encodeBootRecord(boot)))
1303
1275
  }
1304
1276
 
1305
- return Promise.all(proms)
1306
- }
1277
+ await Promise.all(proms)
1307
1278
 
1308
- _storeMigratedHead() {
1309
- const value = this._migratedHead ? encoding.encodeMigratedHead(this._migratedHead) : null
1310
- return this.local.setUserData('autobee/migrated-head', value)
1279
+ if (this._migrating) {
1280
+ // clear legacy data
1281
+ await this.bootstrap.setUserData('autobase/local', null)
1282
+ await this.local.setUserData('autobase/boot', null)
1283
+ await this.local.setUserData('autobase/encryption', null)
1284
+ this._migrating = false
1285
+ }
1311
1286
  }
1312
1287
 
1313
1288
  static decodeValue(buf, opts) {
@@ -1404,11 +1379,11 @@ module.exports = class Autobee extends ReadyResource {
1404
1379
  this._localViewLength = 0
1405
1380
  }
1406
1381
 
1407
- async moveTo(head) {
1382
+ async moveTo(head, { timeout = 0 } = {}) {
1408
1383
  if (!this._bootGuard.opened) await this._bootGuard.ready()
1409
1384
  if (this.closing) throw new Error('Autobee closed')
1410
1385
 
1411
- const ff = await FastForward.fromHead(this, head, null, { force: true })
1386
+ const ff = await FastForward.fromHead(this, head, null, { force: true, timeout })
1412
1387
  if (ff === null) return null
1413
1388
 
1414
1389
  if (!(await this._runFastForward(ff))) return null
@@ -1427,108 +1402,20 @@ module.exports = class Autobee extends ReadyResource {
1427
1402
  }
1428
1403
  }
1429
1404
 
1430
- // the boot head seeds the view every trust decision here is made against,
1431
- // since our own view is still empty
1432
- async _bootFromHead(head, bootCondition, wait = false) {
1433
- // just a head: one attempt, we do not wait around if it cannot be read
1434
- if (bootCondition === null && !wait) {
1435
- try {
1436
- const ff = await FastForward.fromHead(this, head, null, {
1437
- force: true,
1438
- timeout: FastForward.DEFAULT_TIMEOUT
1439
- })
1440
-
1441
- return ff !== null && (await this._runFastForward(ff))
1442
- } catch (err) {
1443
- safetyCatch(err)
1444
- return false
1445
- }
1446
- }
1447
-
1448
- // park until the head can be read and, if a condition is set, something
1449
- // acceptable turns up
1450
- let opened = null
1405
+ async _bootFromHead(head) {
1406
+ const timeout = FastForward.DEFAULT_TIMEOUT
1451
1407
 
1452
1408
  try {
1453
- while (!this._interrupting) {
1454
- this._bootWait = rrp()
1455
-
1456
- try {
1457
- if (bootCondition === null) {
1458
- const ff = await FastForward.fromHead(this, head, null, {
1459
- force: true,
1460
- timeout: FastForward.DEFAULT_TIMEOUT
1461
- })
1462
-
1463
- if (ff !== null && (await this._runFastForward(ff))) return true
1464
- } else {
1465
- if (opened === null) opened = await this._bootReference(head)
1466
-
1467
- if (opened !== null) {
1468
- if (await this._bootAttempt(head, bootCondition, opened.view)) {
1469
- return true
1470
- }
1471
-
1472
- // the reference may be stale - resolve it afresh next attempt
1473
- await opened.close()
1474
- opened = null
1475
- } else if (await this._bootLegacyHead(head)) {
1476
- // boot legacy head through the migration pathway
1477
- return true
1478
- }
1479
- }
1480
- } catch (err) {
1481
- safetyCatch(err)
1482
- }
1409
+ const oplog = await this._resolveOplogHint(head.key, head.length || 0, { timeout })
1410
+ if (oplog === null) return false
1483
1411
 
1484
- if (this._interrupting) break
1412
+ const ff = await FastForward.fromHead(this, oplog, null, { force: true, timeout })
1485
1413
 
1486
- await this._bootWait.promise
1487
- }
1488
- } finally {
1489
- this._bootWait = null
1490
- if (opened !== null) await opened.close()
1414
+ return ff !== null && (await this._runFastForward(ff))
1415
+ } catch (err) {
1416
+ safetyCatch(err)
1417
+ return false
1491
1418
  }
1492
-
1493
- return false
1494
- }
1495
-
1496
- async _bootAttempt(head, bootCondition, reference) {
1497
- // peek, so the hints are still there for the drain once we have booted
1498
- const result = await this._filterHints(this._wakeup.hints)
1499
-
1500
- const candidates = await this._readCandidateHeads(result, FastForward.DEFAULT_TIMEOUT)
1501
-
1502
- const ff = await FastForward.fromHeads(this, [head, ...candidates], {
1503
- force: true,
1504
- timeout: FastForward.DEFAULT_TIMEOUT,
1505
- condition: bootCondition,
1506
- reference
1507
- })
1508
-
1509
- return ff !== null && (await this._runFastForward(ff))
1510
- }
1511
-
1512
- async _bootLegacyHead(head) {
1513
- const oplog = await FastForward.flushHead(this, head, {
1514
- timeout: FastForward.DEFAULT_TIMEOUT
1515
- })
1516
- if (oplog === null || oplog.op.version > LEGACY_OPLOG_VERSION) return false
1517
-
1518
- const ff = await FastForward.fromHead(this, head, null, {
1519
- force: true,
1520
- timeout: FastForward.DEFAULT_TIMEOUT
1521
- })
1522
-
1523
- return ff !== null && (await this._runFastForward(ff))
1524
- }
1525
-
1526
- async _bootReference(head) {
1527
- const oplog = await FastForward.flushHead(this, head, {
1528
- timeout: FastForward.DEFAULT_TIMEOUT
1529
- })
1530
-
1531
- return oplog === null ? null : this.openViewAt(oplog)
1532
1419
  }
1533
1420
 
1534
1421
  async _runFastForward(ff) {
@@ -1554,34 +1441,32 @@ module.exports = class Autobee extends ReadyResource {
1554
1441
  return true
1555
1442
  }
1556
1443
 
1444
+ async _runMigration() {
1445
+ if (!this._handlers.migrate) {
1446
+ throw new Error('Missing migration handler')
1447
+ }
1448
+
1449
+ const migration = this.system.migration
1450
+ this.system.migration = null
1451
+
1452
+ await this._handlers.migrate(migration.views, migration.head)
1453
+ }
1454
+
1557
1455
  async _applyFastForward() {
1558
1456
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
1559
1457
  if (changes) changes.track()
1560
1458
 
1561
- const { head, migrate } = this.fastForwardTo
1459
+ const { head } = this.fastForwardTo
1562
1460
 
1563
1461
  const from = this.system.bee.head()
1564
1462
  const to = head
1565
1463
 
1566
1464
  this.system.bee.move(head)
1567
1465
  await this.system.reset()
1466
+ if (this.system.migration) await this._runMigration()
1568
1467
 
1569
- // migrate is set when fast-forwarding from a legacy head
1570
- if (migrate) {
1571
- const view = (await this._handlers.migrate(migrate, head)) || EMPTY_HEAD
1572
-
1573
- this._migratedHead = {
1574
- system: head,
1575
- view: view.length ? view : (this._migratedHead && this._migratedHead.view) || null
1576
- }
1577
-
1578
- await this._storeMigratedHead()
1579
- this.bee.move(view)
1580
- this._workingBee.move(view)
1581
- } else {
1582
- this.bee.move(this.system.view)
1583
- this._workingBee.move(this.system.view)
1584
- }
1468
+ this.bee.move(this.system.view)
1469
+ this._workingBee.move(this.system.view)
1585
1470
 
1586
1471
  this._approvalCheck = true
1587
1472
  this.fastForwardTo = null
@@ -1593,7 +1478,7 @@ module.exports = class Autobee extends ReadyResource {
1593
1478
  // we moved, so ask our peers to tell us their heads again
1594
1479
  this._requestWakeup()
1595
1480
 
1596
- await this._update(changes)
1481
+ await this._update(changes, true)
1597
1482
  await this._storeBoot()
1598
1483
 
1599
1484
  this.stats.fastForwards++
@@ -1614,7 +1499,7 @@ function getBootOption(boot) {
1614
1499
  if (!boot) return null
1615
1500
 
1616
1501
  // oldest style, supported for now but will go away: a bare key is legacy
1617
- if (boot.key) return { legacy: boot, head: null, bootCondition: null }
1502
+ if (boot.key) return { legacy: boot, head: null }
1618
1503
 
1619
1504
  asserts.assert(!(boot.head && boot.legacy), 'Boot from either a head or a legacy pointer')
1620
1505
 
package/lib/boot.js CHANGED
@@ -5,7 +5,6 @@ const { checkAutobaseMigration } = require('./migrations.js')
5
5
  module.exports = async function boot(
6
6
  corestore,
7
7
  key,
8
- legacyViews,
9
8
  { encrypt, encryptionKey, keyPair, exclusive = true } = {}
10
9
  ) {
11
10
  const result = {
@@ -95,12 +94,7 @@ module.exports = async function boot(
95
94
  await result.bootstrap.setUserData('autobee/local', result.local.key)
96
95
  await result.local.setUserData('referrer', result.key)
97
96
 
98
- result.migration = await migrateWithFallback(
99
- corestore,
100
- result.local,
101
- result.bootstrap,
102
- legacyViews
103
- )
97
+ result.migration = await migrateWithFallback(corestore, result.local, result.bootstrap)
104
98
 
105
99
  if (result.migration) {
106
100
  await result.local.setUserData(
@@ -142,9 +136,9 @@ module.exports = async function boot(
142
136
  // the bootstrap pointer names the rotated-to writer, whose legacy boot record
143
137
  // can be unusable. the namespace local still carries the pre-rotation record,
144
138
  // which points at the last system core that was actually signed
145
- async function migrateWithFallback(corestore, local, bootstrap, legacyViews) {
139
+ async function migrateWithFallback(corestore, local, bootstrap) {
146
140
  try {
147
- return await checkAutobaseMigration(corestore, local, bootstrap, legacyViews)
141
+ return await checkAutobaseMigration(corestore, local, bootstrap)
148
142
  } catch (err) {
149
143
  const prev = corestore.get({ name: 'local', active: false })
150
144
  await prev.ready()
@@ -152,7 +146,7 @@ async function migrateWithFallback(corestore, local, bootstrap, legacyViews) {
152
146
  if (b4a.equals(prev.key, local.key)) throw err
153
147
  if (!(await prev.getUserData('autobase/boot'))) throw err
154
148
 
155
- return checkAutobaseMigration(corestore, prev, bootstrap, legacyViews)
149
+ return checkAutobaseMigration(corestore, prev, bootstrap)
156
150
  }
157
151
  }
158
152
 
package/lib/encoding.js CHANGED
@@ -12,19 +12,16 @@ const SystemInfo = getEncoding('@autobee/system-info')
12
12
  const SystemWriter = getEncoding('@autobee/system-writer')
13
13
  const ManifestData = getEncoding('@autobee/manifest-data')
14
14
  const AutobaseBootRecord = getEncoding('@autobase-compat/boot-record')
15
- const MigratedHead = getEncoding('@autobee/migrated-head')
16
15
 
17
16
  exports.OPLOG_VERSION = OPLOG_VERSION
18
17
 
19
18
  exports.Oplog = Oplog
19
+
20
20
  exports.ManifestData = ManifestData
21
21
 
22
22
  exports.encodeBootRecord = encodeBootRecord
23
23
  exports.decodeBootRecord = decodeBootRecord
24
24
 
25
- exports.encodeMigratedHead = encodeMigratedHead
26
- exports.decodeMigratedHead = decodeMigratedHead
27
-
28
25
  exports.encodePreviousDrain = encodePreviousDrain
29
26
  exports.decodePreviousDrain = decodePreviousDrain
30
27
 
@@ -50,14 +47,6 @@ function decodeBootRecord(value) {
50
47
  return c.decode(Link, value)
51
48
  }
52
49
 
53
- function encodeMigratedHead(m) {
54
- return c.encode(MigratedHead, m)
55
- }
56
-
57
- function decodeMigratedHead(value) {
58
- return c.decode(MigratedHead, value)
59
- }
60
-
61
50
  function encodePreviousDrain(d) {
62
51
  return c.encode(c.uint, d)
63
52
  }
@@ -1,10 +1,8 @@
1
1
  const safetyCatch = require('safety-catch')
2
- const b4a = require('b4a')
3
2
  const Hypercore = require('hypercore')
4
3
 
5
4
  const System = require('./system.js')
6
5
  const migrations = require('./migrations.js')
7
- const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
8
6
 
9
7
  const DEFAULT_OP_TIMEOUT = 5_000
10
8
  const MIN_FF_GAP = 32
@@ -17,7 +15,7 @@ module.exports = class FastForward {
17
15
 
18
16
  // 'reboot' is a storage namespace: renaming it would derive a different
19
17
  // scratch core on disk, so it stays as is
20
- this.system = new System(auto.store.namespace('reboot'), null, {
18
+ this.system = new System(auto, auto.store.namespace('reboot'), {
21
19
  getEncryptionProvider: this.auto.getSystemEncryption,
22
20
  encrypted: this.encrypted,
23
21
  activeRequests: this.activeRequests
@@ -34,12 +32,7 @@ module.exports = class FastForward {
34
32
 
35
33
  static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
36
34
 
37
- static async fromHead(
38
- auto,
39
- head,
40
- trusted,
41
- { force = false, timeout = 0, condition = null, reference = null } = {}
42
- ) {
35
+ static async fromHead(auto, head, trusted, { force = false, timeout = 0 } = {}) {
43
36
  const conservative = !force && auto._conservativeFF
44
37
  const timeoutOpts = timeout ? { timeout } : null
45
38
 
@@ -59,19 +52,11 @@ module.exports = class FastForward {
59
52
  return null
60
53
  }
61
54
 
62
- if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
63
- return null
64
- }
65
-
66
55
  return new FastForward(auto, batchToHead(verified.op.views.system))
67
56
  }
68
57
 
69
- static async fromHeads(
70
- auto,
71
- heads,
72
- { force = false, timeout = 0, condition = null, reference = null } = {}
73
- ) {
74
- if (reference === null) reference = auto._workingView.view
58
+ static async fromHeads(auto, heads, { force = false, timeout = 0 } = {}) {
59
+ const reference = auto._workingView.view
75
60
 
76
61
  const promises = []
77
62
 
@@ -112,12 +97,7 @@ module.exports = class FastForward {
112
97
  }
113
98
 
114
99
  if (bestTrusted !== null) {
115
- return FastForward.fromHead(auto, bestTrusted, null, {
116
- force,
117
- timeout,
118
- condition,
119
- reference
120
- })
100
+ return FastForward.fromHead(auto, bestTrusted, null, { force, timeout })
121
101
  }
122
102
 
123
103
  candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
@@ -127,12 +107,7 @@ module.exports = class FastForward {
127
107
  if (trusted === null) continue
128
108
  if (auto.fastForwarding || auto.fastForwardTo) return null
129
109
 
130
- const ff = await FastForward.fromHead(auto, res, trusted, {
131
- force,
132
- timeout,
133
- condition,
134
- reference
135
- })
110
+ const ff = await FastForward.fromHead(auto, res, trusted, { force, timeout })
136
111
  if (ff !== null) return ff
137
112
  }
138
113
 
@@ -151,17 +126,6 @@ module.exports = class FastForward {
151
126
  }
152
127
  }
153
128
 
154
- static async accepts(auto, oplog, condition, reference) {
155
- const opened = auto.openViewAt(oplog)
156
- if (opened === null) return false
157
-
158
- try {
159
- return !!(await condition(opened.view, reference))
160
- } finally {
161
- await opened.close()
162
- }
163
- }
164
-
165
129
  static async mostRecentTrusted(auto, head, reference) {
166
130
  const opened = auto.openViewAt(head)
167
131
  if (opened === null) return null
@@ -198,38 +162,6 @@ module.exports = class FastForward {
198
162
 
199
163
  await this.system.boot(this.head, { timeout: this.timeout })
200
164
 
201
- // a legacy system needs migrating rather than a plain fast-forward
202
- if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
203
- const migrated = this.auto._migratedHead && this.auto._migratedHead.system
204
- if (
205
- migrated &&
206
- b4a.equals(migrated.key, this.head.key) &&
207
- migrated.length >= this.head.length
208
- ) {
209
- return null
210
- }
211
-
212
- const result = await this._migrate()
213
- if (result === null || result.autobee === null) return result
214
-
215
- // an indexer of the newest legacy generation already moved to autobee -
216
- // fast-forward straight onto its announced head instead of migrating,
217
- // falling back to the legacy migration if the head cannot be booted
218
- try {
219
- this.head = result.autobee
220
- await this.system.boot(this.head, { timeout: this.timeout })
221
- if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
222
- throw new Error('Expected an autobee system')
223
- }
224
- } catch (err) {
225
- safetyCatch(err)
226
- this.head = result.head
227
- return { head: result.head, migrate: result.migrate }
228
- }
229
-
230
- // fall through to the plain fast-forward below
231
- }
232
-
233
165
  const promises = []
234
166
 
235
167
  // ensure local key is locally available always
@@ -252,70 +184,12 @@ module.exports = class FastForward {
252
184
  await Promise.all(promises)
253
185
  if (this.destroyed) return null
254
186
 
255
- if (this.destroyed) return null
256
-
257
187
  return {
258
188
  head: this.head,
259
189
  migrate: null
260
190
  }
261
191
  }
262
192
 
263
- // Upgrade a legacy (pre-AUTOBEE_VERSION) system in place. The system already
264
- // booted above (coerced to the current struct), but the original v1/v2 struct
265
- // is needed to recover the indexers/views/entropy, so decode the raw block.
266
- //
267
- // A head may be arbitrarily old: an indexer rotation freezes the system core
268
- // and moves the indexers to a successor. The frozen core cannot reveal its
269
- // successor (the promotion only lands in its unsigned tail), but the
270
- // indexers' newest oplog digests stamp the newest system they know - chase
271
- // those so the migration lands on the newest generation instead of the head
272
- // we were given.
273
- async _migrate() {
274
- let head = { key: this.head.key, length: this.head.length }
275
- const seen = new Set()
276
-
277
- for (let hops = 0; hops < migrations.MAX_MIGRATE_HOPS; hops++) {
278
- seen.add(b4a.toString(head.key, 'hex'))
279
-
280
- const core = this.auto.store.get({ key: head.key, encryption: null })
281
- this.cores.push(core)
282
- await core.ready()
283
-
284
- // setup encryption
285
- await core.setEncryption(this.auto.getSystemEncryption())
286
-
287
- // successors are resolved by key only - wait for the replicated length
288
- if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
289
- if (!head.length || this.destroyed) return null
290
-
291
- const info = await migrations.readLegacySystemInfo(core, head.length, {
292
- timeout: this.timeout,
293
- activeRequests: this.activeRequests
294
- })
295
-
296
- const { legacy, autobee } = await migrations.findNewestSystem(this, info)
297
- if (this.destroyed) return null
298
-
299
- if (legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
300
- head = { key: legacy, length: 0 }
301
- continue
302
- }
303
-
304
- this.head = head
305
-
306
- const views = await migrations.resolveLegacyViews(this, info)
307
- if (views === null || this.destroyed) return null
308
-
309
- return {
310
- head,
311
- migrate: views,
312
- autobee: autobee === null ? null : batchToHead(autobee.op.views.system)
313
- }
314
- }
315
-
316
- return null
317
- }
318
-
319
193
  async _resolveLength() {
320
194
  const core = this.auto.store.get({ key: this.head.key })
321
195
  this.cores.push(core)
package/lib/migrations.js CHANGED
@@ -6,7 +6,6 @@ const { decodeBlock } = require('hyperbee2/lib/encoding.js')
6
6
 
7
7
  const encoding = require('./encoding.js')
8
8
  const { assert, bail } = require('./asserts.js')
9
- const { LEGACY_OPLOG_VERSION } = require('./constants.js')
10
9
 
11
10
  const { getEncoding } = require('../encoding/spec/autobee')
12
11
 
@@ -18,23 +17,52 @@ const INDEX_VERSION = 1
18
17
  const INFO = b4a.from([0x00, 0x00, 0x69, 0x6e, 0x66, 0x6f])
19
18
  const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
20
19
 
21
- // indexer rotations chain legacy systems - how many generations we chase
22
- const MAX_MIGRATE_HOPS = 64
23
-
24
20
  module.exports = {
25
- MAX_MIGRATE_HOPS,
21
+ getCoreManifest,
22
+ getViewMap,
26
23
  checkAutobaseMigration,
27
- readLegacySystemInfo,
28
- findNewestSystem,
29
- resolveLegacyViews,
30
24
  inflateLegacyOplog,
31
- decodeLegacySystemInfo,
32
- deriveNamespace,
33
25
  coreLength
34
26
  }
35
27
 
28
+ async function getCoreManifest(store, key, { timeout, wait = true } = {}) {
29
+ const core = store.get(key)
30
+ await core.ready()
31
+ try {
32
+ if (!core.manifest && core.length === 0) await core.get(0, { timeout, wait })
33
+ return core.manifest
34
+ } finally {
35
+ await core.close()
36
+ }
37
+ }
38
+
39
+ // local-only probes: the manifests are expected in storage already (prefetched
40
+ // by the reset path), so a violation skips instead of hanging on the network
41
+ async function getViewMap(store, bootstrap, encryptionKey, info, legacyViews) {
42
+ const getIndexerManifest = ({ key }) => getCoreManifest(store, key, { wait: false })
43
+ const indexerManifests = await Promise.all(info.indexers.map(getIndexerManifest))
44
+ const entropy = legacyEntropy(info, indexerManifests)
45
+ const mappedViews = new Map()
46
+
47
+ for (const name of legacyViews) {
48
+ const v = await findViewByName(
49
+ store,
50
+ bootstrap.key,
51
+ encryptionKey,
52
+ indexerManifests,
53
+ info.views,
54
+ entropy,
55
+ name
56
+ )
57
+
58
+ mappedViews.set(name, v)
59
+ }
60
+
61
+ return mappedViews
62
+ }
63
+
36
64
  // boot-time migration of local legacy autobase storage
37
- async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
65
+ async function checkAutobaseMigration(store, local, bootstrap) {
38
66
  const bootRecord = await local.getUserData('autobase/boot')
39
67
  if (!bootRecord) return null
40
68
 
@@ -60,41 +88,19 @@ async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
60
88
  await session.close()
61
89
  }
62
90
 
63
- const info = await readLegacySystemInfo(core, length, { wait: false })
91
+ // nothing reads the info here anymore, but a legacy system that will not
92
+ // decode has to throw for migrateWithFallback pathway
93
+ await readLegacySystemInfo(core, length, { wait: false })
64
94
 
65
95
  const nodes = await Promise.all(
66
96
  catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
67
97
  )
68
98
 
69
- const indexerManifests = await Promise.all(
70
- info.indexers.map((idx) => storeCoreManifest(store, idx.key))
71
- )
72
- const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
73
- const entropy = legacyEntropy(info, indexerManifests)
74
-
75
- const getManifest = (key) => storeCoreManifest(store, key)
76
-
77
- const mappedViews = new Map()
78
- for (const name of legacyViews) {
79
- const v = await findViewByName(
80
- getManifest,
81
- bootstrap.key,
82
- encryptionKey,
83
- indexerManifests,
84
- views,
85
- entropy,
86
- name
87
- )
88
-
89
- mappedViews.set(name, v)
90
- }
91
-
92
99
  await core.close()
93
100
 
94
101
  return {
95
102
  encryptionKey,
96
103
  system,
97
- views: mappedViews,
98
104
  catchup: nodes
99
105
  }
100
106
  }
@@ -112,95 +118,6 @@ async function readLegacySystemInfo(core, length, opts = null) {
112
118
  return decodeLegacySystemInfo(node.keys[0].value)
113
119
  }
114
120
 
115
- // read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
116
- // the newest legacy system that indexer knows. take the majority vote. an
117
- // indexer that already moved to autobee announces its live autobee state
118
- // instead - keep the newest of those as a direct fast-forward candidate and
119
- // bisect down to its buried legacy stamps so its vote still counts
120
- async function findNewestSystem(ff, info) {
121
- const tally = new Map()
122
-
123
- let autobee = null
124
-
125
- for (const idx of info.indexers) {
126
- const core = ff.auto.openCore(idx.key)
127
- ff.cores.push(core)
128
- await core.ready()
129
-
130
- const length = await coreLength(core, ff.timeout)
131
- if (!length || ff.destroyed) continue
132
-
133
- let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
134
- if (oplog === null || !oplog.op.views) continue
135
-
136
- if (oplog.op.version > LEGACY_OPLOG_VERSION) {
137
- if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
138
- autobee = oplog
139
- }
140
-
141
- const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
142
- if (!boundary || ff.destroyed) continue
143
-
144
- oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
145
- if (oplog === null || !oplog.op.views) continue
146
- if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
147
- }
148
-
149
- const sys = oplog.op.views.system
150
- if (!sys || !sys.key) continue
151
-
152
- const hex = b4a.toString(sys.key, 'hex')
153
- const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
154
-
155
- t.votes++
156
- if (sys.length > t.length) t.length = sys.length
157
- tally.set(hex, t)
158
- }
159
-
160
- const majority = (info.indexers.length >> 1) + 1
161
-
162
- let best = null
163
- for (const t of tally.values()) {
164
- if (t.votes >= majority && (best === null || t.length > best.length)) best = t
165
- }
166
-
167
- return { legacy: best === null ? null : best.key, autobee }
168
- }
169
-
170
- // map the legacy views recorded in the system info by name
171
- async function resolveLegacyViews(ff, info) {
172
- const indexerManifests = await Promise.all(
173
- info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
174
- )
175
- if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
176
-
177
- const entropy = legacyEntropy(info, indexerManifests)
178
-
179
- const getManifest = (key, length) => ffCoreManifest(ff, key, length)
180
-
181
- const views = new Map()
182
-
183
- await Promise.all(
184
- ff.auto.legacyViews.map(async (name) => {
185
- const v = await findViewByName(
186
- getManifest,
187
- ff.auto.key,
188
- ff.auto.encryptionKey,
189
- indexerManifests,
190
- info.views,
191
- entropy,
192
- name
193
- )
194
-
195
- if (v) views.set(name, v)
196
- })
197
- )
198
-
199
- if (ff.destroyed) return null
200
-
201
- return views
202
- }
203
-
204
121
  // legacy nodes always inflate, but only indexers carry the digest/checkpoint
205
122
  // stamps that synthesise system info - callers must check op.views
206
123
  async function inflateLegacyOplog(buf, core, seq, timeout) {
@@ -309,32 +226,12 @@ function coreLength(core, timeout) {
309
226
  })
310
227
  }
311
228
 
312
- // a writer's oplog is a legacy prefix followed by autobee-format nodes -
313
- // bisect for the boundary. lo is a length known to be legacy (the legacy
314
- // system's recorded length for this writer), hi a seq known to be autobee.
315
- // returns the legacy prefix length
316
- async function findLegacyBoundary(ff, core, lo, hi) {
317
- while (lo < hi) {
318
- const mid = (lo + hi) >>> 1
319
-
320
- const block = await core.get(mid, {
321
- timeout: ff.timeout,
322
- activeRequests: ff.activeRequests
323
- })
324
-
325
- if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
326
- else lo = mid + 1
327
- }
328
-
329
- return lo
330
- }
331
-
332
229
  function legacyEntropy(info, indexerManifests) {
333
230
  return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
334
231
  }
335
232
 
336
233
  async function findViewByName(
337
- getManifest,
234
+ store,
338
235
  bootstrap,
339
236
  encryptionKey,
340
237
  indexerManifests,
@@ -347,7 +244,7 @@ async function findViewByName(
347
244
  const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
348
245
 
349
246
  for (const v of views) {
350
- const manifest = await getManifest(v.key, v.length)
247
+ const manifest = await getCoreManifest(store, v.key, { wait: false })
351
248
  if (!manifest) continue
352
249
 
353
250
  if (manifest.signers.length === 0) continue
@@ -360,50 +257,6 @@ async function findViewByName(
360
257
  return null
361
258
  }
362
259
 
363
- // request-tracked manifest read over replication
364
- async function ffCoreManifest(ff, key, length) {
365
- const core = ff.auto.store.get(key)
366
- ff.cores.push(core)
367
-
368
- await core.ready()
369
-
370
- try {
371
- if (!core.manifest) {
372
- await core.get(length - 1, {
373
- timeout: ff.timeout,
374
- activeRequests: ff.activeRequests
375
- })
376
- }
377
- } catch {
378
- return null
379
- }
380
-
381
- return core.manifest
382
- }
383
-
384
- // local-storage manifest read
385
- async function storeCoreManifest(store, key) {
386
- const core = store.get(key)
387
- await core.ready()
388
-
389
- const manifest = core.manifest
390
- await core.close()
391
-
392
- return manifest
393
- }
394
-
395
- async function getPersistedView(store, view) {
396
- const core = store.get(view.key)
397
- await core.ready()
398
-
399
- // core.length can run past what was downloaded - the system's recorded
400
- // length is the last one we know is local
401
- const length = Math.min(core.length, view.length)
402
- await core.close()
403
-
404
- return { key: view.key, length }
405
- }
406
-
407
260
  function getOplog(info, batch) {
408
261
  if (batch.length === 1) return batch[0].keys[0]
409
262
 
@@ -489,9 +342,3 @@ async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
489
342
 
490
343
  return batch
491
344
  }
492
-
493
- // lazy to avoid the require cycle - fast-forward imports this module
494
- function flushHead(auto, head, opts) {
495
- const FastForward = require('./fast-forward.js')
496
- return FastForward.flushHead(auto, head, opts)
497
- }
package/lib/system.js CHANGED
@@ -2,7 +2,9 @@ const Hyperbee = require('hyperbee2')
2
2
  const Promotions = require('./promotions.js')
3
3
  const b4a = require('b4a')
4
4
 
5
- const { AUTOBEE_VERSION } = require('./constants')
5
+ const { getViewMap, getCoreManifest } = require('./migrations.js')
6
+
7
+ const { AUTOBEE_VERSION, LEGACY_AUTOBASE_VERSION } = require('./constants')
6
8
  const encoding = require('./encoding.js')
7
9
  const topo = require('./topo.js')
8
10
 
@@ -33,12 +35,13 @@ class SystemSnapshot {
33
35
  }
34
36
 
35
37
  module.exports = class Systembee {
36
- constructor(store, name, opts = {}) {
37
- this.name = name // for debuggin
38
+ constructor(auto, store, opts = {}) {
39
+ this.auto = auto
38
40
  this.store = store
39
41
  this.bee = new Hyperbee(store, opts)
40
42
  this.view = null
41
43
  this.heads = []
44
+ this.migration = null
42
45
  this.version = 0
43
46
  this.timestamp = 0
44
47
  this.flushes = 0
@@ -48,6 +51,10 @@ module.exports = class Systembee {
48
51
  this.promotions = new Promotions(this.bee)
49
52
  }
50
53
 
54
+ get name() {
55
+ return this.auto.name
56
+ }
57
+
51
58
  async addWriter(
52
59
  key,
53
60
  { length = 0, weight = 1, isGenesis = false, coord = null, carrier = -1, anchor = false } = {}
@@ -127,9 +134,17 @@ module.exports = class Systembee {
127
134
 
128
135
  this.version = info ? info.version : 0
129
136
  this.view = info ? info.view : EMPTY_HEAD
137
+ this.migration = null
130
138
  this.heads = info ? info.heads : []
131
139
  this.timestamp = info ? info.timestamp : 0
132
140
  this.flushes = info ? info.flushes : 0
141
+
142
+ if (this.version <= LEGACY_AUTOBASE_VERSION) {
143
+ // ~~~ good enough estimation
144
+ this.flushes = Math.floor(this.bee.core.length / 2)
145
+ this.view = await this._migrate(info, { timeout })
146
+ }
147
+
133
148
  this.updates.clear()
134
149
  this.writers.clear()
135
150
  this.promotions.reset(info)
@@ -139,6 +154,29 @@ module.exports = class Systembee {
139
154
  }
140
155
  }
141
156
 
157
+ async _migrate(info, { timeout } = {}) {
158
+ if (!info) return EMPTY_HEAD
159
+ const a = this.auto
160
+
161
+ const toManifests = ({ key }) => getCoreManifest(a.store, key, { timeout })
162
+ const promises = [...info.indexers.map(toManifests), ...info.views.map(toManifests)]
163
+
164
+ await Promise.all(promises)
165
+
166
+ const views = await getViewMap(a.store, a.bootstrap, a.encryptionKey, info, a.legacyViews)
167
+
168
+ // the handler only runs once the head is locked in - see auto._runMigration
169
+ this.migration = { views, head: this.bee.head() }
170
+
171
+ // legacyViews is ordered by preference, so the first one that resolved wins
172
+ for (const name of a.legacyViews) {
173
+ const view = views.get(name)
174
+ if (view) return view
175
+ }
176
+
177
+ return EMPTY_HEAD
178
+ }
179
+
142
180
  bootRecord() {
143
181
  const system = this.bee.head()
144
182
  return system.length ? system : null
@@ -441,21 +479,3 @@ module.exports = class Systembee {
441
479
  return this.promotions.hint(key, opts)
442
480
  }
443
481
  }
444
-
445
- function getInfo(data) {
446
- for (const { keys } of data.batch) {
447
- for (const k of keys) {
448
- const prefix = k.key[0]
449
-
450
- if (prefix === 0) {
451
- try {
452
- return encoding.decodeSystemInfo(k.value)
453
- } catch {
454
- return null
455
- }
456
- }
457
- }
458
- }
459
-
460
- return null
461
- }
package/lib/topo.js CHANGED
@@ -7,15 +7,11 @@ const { LEGACY_OPLOG_VERSION } = require('./constants.js')
7
7
  const EMPTY_HEAD = { length: 0, key: null }
8
8
  const INFO_LEGACY_KEY = b4a.concat([b4a.from([0, 0]), b4a.from('info')])
9
9
 
10
- const DEFAULT_OP_TIMEOUT = 5000
11
-
12
10
  exports.sort = sort
13
11
  exports.replay = replay
14
- exports.getMostRecentHead = getMostRecentHead
15
12
 
16
13
  exports.isLinking = isLinking
17
14
  exports.isLinkingAll = isLinkingAll
18
- exports.isLegacyNode = isLegacyNode
19
15
 
20
16
  class Clock {
21
17
  constructor() {
@@ -35,19 +31,6 @@ class Clock {
35
31
  }
36
32
  }
37
33
 
38
- async function getMostRecentHead(auto, system) {
39
- const ch = system.createChangesStream()
40
- for await (const data of ch) {
41
- const refs = getOplogCompat(data)
42
- if (!refs.length) continue
43
-
44
- const { oplog } = refs[0]
45
- return { key: oplog.key, length: oplog.length }
46
- }
47
-
48
- return null
49
- }
50
-
51
34
  // nodes we cannot resolve are returned as null rather than dropped, so callers
52
35
  // can tell an incomplete replay from a short one
53
36
  async function replay(auto) {
@@ -181,7 +164,7 @@ function cmp(a, b) {
181
164
  return a.length < b.length ? -1 : 1
182
165
  }
183
166
 
184
- async function inflate(auto, reorder, { wait = true, timeout = DEFAULT_OP_TIMEOUT } = {}) {
167
+ async function inflate(auto, reorder, { wait = true, timeout = 0 } = {}) {
185
168
  const readOpts = wait ? { timeout } : { wait: false }
186
169
 
187
170
  const promises = []
@@ -233,7 +216,7 @@ async function getOplogBatch(undo, core, length, weight, index, readOpts = null)
233
216
  const legacy = oplog.version <= LEGACY_OPLOG_VERSION
234
217
 
235
218
  // if this is legacy the start pointer is always 0, fix
236
- if (legacy) {
219
+ if (legacy && !oplog.links.length) {
237
220
  for (let i = length - 1; i > 0; i--) {
238
221
  const block = await core.get(i - 1, readOpts)
239
222
  if (block === null) return null // wait: false in replay helper
@@ -241,6 +224,7 @@ async function getOplogBatch(undo, core, length, weight, index, readOpts = null)
241
224
  const oplog = encoding.decodeOplog(block)
242
225
  if (oplog.batch.end === 0) break
243
226
  head.batch.start++
227
+ if (oplog.links.length > 0) break // always start of batch in legacy
244
228
  }
245
229
  }
246
230
 
@@ -345,6 +329,8 @@ function getOplogCompat(data) {
345
329
  const candidates = []
346
330
 
347
331
  for (const { keys } of data.batch) {
332
+ if (!keys) continue
333
+
348
334
  for (const k of keys) {
349
335
  if (k.key[0] !== 1) continue
350
336
 
package/lib/updates.js CHANGED
@@ -24,7 +24,7 @@ module.exports = class UpdateChanges {
24
24
  }
25
25
  }
26
26
 
27
- finalise() {
27
+ finalise(fastForward) {
28
28
  // flush count at `to` - safe to read live, state is frozen for the hook
29
29
  const flushes = this.auto.system.flushes
30
30
 
@@ -33,6 +33,7 @@ module.exports = class UpdateChanges {
33
33
 
34
34
  tracked.to = this.tracking[k].head()
35
35
  tracked.flushes = flushes
36
+ tracked.incremental = !fastForward
36
37
  }
37
38
 
38
39
  return this.byName
package/lib/writers.js CHANGED
@@ -552,15 +552,6 @@ class Writer {
552
552
  node.timestamp = timestamp = Math.max(node.timestamp, timestamp)
553
553
  node.optimistic = optimistic = optimistic || node.optimistic
554
554
 
555
- if (!this.isAdded && !node.optimistic && !genesis) {
556
- return null
557
- }
558
-
559
- // an optimistic writer cannot be genesis
560
- if (node.optimistic && genesis) {
561
- return null
562
- }
563
-
564
555
  if (node.optimistic && !this.isAdded && b.end >= BATCH_OPTIMISTIC_SANITY) {
565
556
  return null
566
557
  }
@@ -578,6 +569,15 @@ class Writer {
578
569
  return null
579
570
  }
580
571
 
572
+ // an optimistic writer cannot be genesis
573
+ if (node.optimistic && genesis) {
574
+ return null
575
+ }
576
+
577
+ if (!this.isAdded && !node.optimistic && !genesis) {
578
+ return null
579
+ }
580
+
581
581
  batch.push(node)
582
582
  if (b.end === 0) return finalizeNodeBatch(batch)
583
583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",