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 +8 -7
- package/index.js +58 -173
- package/lib/boot.js +4 -10
- package/lib/encoding.js +1 -12
- package/lib/fast-forward.js +6 -132
- package/lib/migrations.js +44 -197
- package/lib/system.js +41 -21
- package/lib/topo.js +5 -19
- package/lib/updates.js +2 -1
- package/lib/writers.js +9 -9
- package/package.json +1 -1
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 },
|
|
251
|
-
legacy: { key, length }
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
518
|
-
|
|
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
|
|
639
|
+
const { head = null, legacy = null } = this.bootFrom
|
|
672
640
|
|
|
673
641
|
this.bootFrom = null
|
|
674
642
|
|
|
675
|
-
if (legacy)
|
|
676
|
-
|
|
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
|
-
|
|
1306
|
-
}
|
|
1277
|
+
await Promise.all(proms)
|
|
1307
1278
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1454
|
-
|
|
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
|
-
|
|
1412
|
+
const ff = await FastForward.fromHead(this, oplog, null, { force: true, timeout })
|
|
1485
1413
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
|
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
|
-
|
|
1570
|
-
|
|
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
|
|
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
|
|
139
|
+
async function migrateWithFallback(corestore, local, bootstrap) {
|
|
146
140
|
try {
|
|
147
|
-
return await checkAutobaseMigration(corestore, local, bootstrap
|
|
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
|
|
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
|
}
|
package/lib/fast-forward.js
CHANGED
|
@@ -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'),
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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(
|
|
37
|
-
this.
|
|
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 =
|
|
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
|
|