autobee 2.0.0-rc.1 → 2.0.0-rc.11
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/encoding/compat.js +1 -1
- package/encoding/legacy.js +3 -3
- package/encoding/spec/autobee/index.js +27 -19
- package/encoding/spec/autobee/schema.json +11 -1
- package/index.js +139 -42
- package/lib/apply-calls.js +5 -0
- package/lib/system.js +107 -30
- package/lib/topo.js +105 -14
- package/lib/writers.js +4 -1
- package/package.json +2 -2
package/encoding/compat.js
CHANGED
package/encoding/legacy.js
CHANGED
|
@@ -153,18 +153,18 @@ const Node = {
|
|
|
153
153
|
preencode(state, m) {
|
|
154
154
|
Clock.preencode(state, m.heads)
|
|
155
155
|
c.uint.preencode(state, m.batch)
|
|
156
|
-
c.
|
|
156
|
+
c.optionalBuffer.preencode(state, m.value)
|
|
157
157
|
},
|
|
158
158
|
encode(state, m) {
|
|
159
159
|
Clock.encode(state, m.heads)
|
|
160
160
|
c.uint.encode(state, m.batch)
|
|
161
|
-
c.
|
|
161
|
+
c.optionalBuffer.encode(state, m.value)
|
|
162
162
|
},
|
|
163
163
|
decode(state, m) {
|
|
164
164
|
return {
|
|
165
165
|
heads: Clock.decode(state),
|
|
166
166
|
batch: c.uint.decode(state),
|
|
167
|
-
value: c.
|
|
167
|
+
value: c.optionalBuffer.decode(state)
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -97,8 +97,9 @@ const encoding4 = {
|
|
|
97
97
|
// @autobase-compat/boot-record
|
|
98
98
|
const encoding5 = {
|
|
99
99
|
preencode(state, m) {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const v = m.version ?? 3
|
|
101
|
+
c.uint.preencode(state, v)
|
|
102
|
+
switch (v) {
|
|
102
103
|
case 0:
|
|
103
104
|
encoding3.preencode(state, m)
|
|
104
105
|
break
|
|
@@ -112,8 +113,9 @@ const encoding5 = {
|
|
|
112
113
|
}
|
|
113
114
|
},
|
|
114
115
|
encode(state, m) {
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
const v = m.version ?? 3
|
|
117
|
+
c.uint.encode(state, v)
|
|
118
|
+
switch (v) {
|
|
117
119
|
case 0:
|
|
118
120
|
encoding3.encode(state, m)
|
|
119
121
|
break
|
|
@@ -236,17 +238,17 @@ const encoding9 = {
|
|
|
236
238
|
preencode(state, m) {
|
|
237
239
|
encoding1.preencode(state, m.heads)
|
|
238
240
|
c.uint.preencode(state, m.batch)
|
|
239
|
-
c.
|
|
241
|
+
c.optionalBuffer.preencode(state, m.value)
|
|
240
242
|
},
|
|
241
243
|
encode(state, m) {
|
|
242
244
|
encoding1.encode(state, m.heads)
|
|
243
245
|
c.uint.encode(state, m.batch)
|
|
244
|
-
c.
|
|
246
|
+
c.optionalBuffer.encode(state, m.value)
|
|
245
247
|
},
|
|
246
248
|
decode(state) {
|
|
247
249
|
const r0 = encoding1.decode(state)
|
|
248
250
|
const r1 = c.uint.decode(state)
|
|
249
|
-
const r2 = c.
|
|
251
|
+
const r2 = c.optionalBuffer.decode(state)
|
|
250
252
|
|
|
251
253
|
return {
|
|
252
254
|
heads: r0,
|
|
@@ -496,8 +498,9 @@ const encoding18 = {
|
|
|
496
498
|
// @autobee/system-info
|
|
497
499
|
const encoding19 = {
|
|
498
500
|
preencode(state, m) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
+
const v = m.version ?? 3
|
|
502
|
+
c.uint.preencode(state, v)
|
|
503
|
+
switch (v) {
|
|
501
504
|
case 0:
|
|
502
505
|
case 1:
|
|
503
506
|
encoding15.preencode(state, m)
|
|
@@ -513,8 +516,9 @@ const encoding19 = {
|
|
|
513
516
|
}
|
|
514
517
|
},
|
|
515
518
|
encode(state, m) {
|
|
516
|
-
|
|
517
|
-
|
|
519
|
+
const v = m.version ?? 3
|
|
520
|
+
c.uint.encode(state, v)
|
|
521
|
+
switch (v) {
|
|
518
522
|
case 0:
|
|
519
523
|
case 1:
|
|
520
524
|
encoding15.encode(state, m)
|
|
@@ -603,8 +607,9 @@ const encoding20 = {
|
|
|
603
607
|
// @autobee/system-writer
|
|
604
608
|
const encoding21 = {
|
|
605
609
|
preencode(state, m) {
|
|
606
|
-
|
|
607
|
-
|
|
610
|
+
const v = m.version ?? 4
|
|
611
|
+
c.uint.preencode(state, v)
|
|
612
|
+
switch (v) {
|
|
608
613
|
case 0:
|
|
609
614
|
case 1:
|
|
610
615
|
case 2:
|
|
@@ -619,8 +624,9 @@ const encoding21 = {
|
|
|
619
624
|
}
|
|
620
625
|
},
|
|
621
626
|
encode(state, m) {
|
|
622
|
-
|
|
623
|
-
|
|
627
|
+
const v = m.version ?? 4
|
|
628
|
+
c.uint.encode(state, v)
|
|
629
|
+
switch (v) {
|
|
624
630
|
case 0:
|
|
625
631
|
case 1:
|
|
626
632
|
case 2:
|
|
@@ -905,8 +911,9 @@ const encoding29 = {
|
|
|
905
911
|
// @autobee/oplog
|
|
906
912
|
const encoding30 = {
|
|
907
913
|
preencode(state, m) {
|
|
908
|
-
|
|
909
|
-
|
|
914
|
+
const v = m.version ?? 3
|
|
915
|
+
c.uint.preencode(state, v)
|
|
916
|
+
switch (v) {
|
|
910
917
|
case 0:
|
|
911
918
|
encoding12.preencode(state, m)
|
|
912
919
|
break
|
|
@@ -924,8 +931,9 @@ const encoding30 = {
|
|
|
924
931
|
}
|
|
925
932
|
},
|
|
926
933
|
encode(state, m) {
|
|
927
|
-
|
|
928
|
-
|
|
934
|
+
const v = m.version ?? 3
|
|
935
|
+
c.uint.encode(state, v)
|
|
936
|
+
switch (v) {
|
|
929
937
|
case 0:
|
|
930
938
|
encoding12.encode(state, m)
|
|
931
939
|
break
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
{
|
|
195
195
|
"name": "value",
|
|
196
196
|
"required": true,
|
|
197
|
-
"type": "
|
|
197
|
+
"type": "optionalBuffer",
|
|
198
198
|
"version": 1
|
|
199
199
|
}
|
|
200
200
|
]
|
|
@@ -805,5 +805,15 @@
|
|
|
805
805
|
}
|
|
806
806
|
]
|
|
807
807
|
}
|
|
808
|
+
],
|
|
809
|
+
"namespaces": [
|
|
810
|
+
{
|
|
811
|
+
"name": "autobee",
|
|
812
|
+
"external": "../../legacy.js"
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"name": "autobase-compat",
|
|
816
|
+
"external": "../../legacy.js"
|
|
817
|
+
}
|
|
808
818
|
]
|
|
809
819
|
}
|
package/index.js
CHANGED
|
@@ -88,6 +88,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
88
88
|
|
|
89
89
|
this._localSystemStart = 0
|
|
90
90
|
this._localSystemLength = 0
|
|
91
|
+
this._localFlushes = 0
|
|
91
92
|
this._localViewStart = 0
|
|
92
93
|
this._localViewLength = 0
|
|
93
94
|
|
|
@@ -105,6 +106,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
105
106
|
this._hasApply = !!handlers.apply
|
|
106
107
|
this._hasUpdate = !!handlers.update
|
|
107
108
|
this._needsUpdate = false
|
|
109
|
+
this._ackRequired = false
|
|
110
|
+
this._ackedHeads = new Map()
|
|
108
111
|
this._updateLocalCore = null
|
|
109
112
|
this._host = new ApplyCalls(this)
|
|
110
113
|
this._notifyHandler = null
|
|
@@ -161,6 +164,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
161
164
|
|
|
162
165
|
this._localSystemStart = this.system.bee.context.local.length
|
|
163
166
|
this._localViewStart = this._workingBee.context.local.length
|
|
167
|
+
this._localFlushes = this.system.flushes
|
|
164
168
|
|
|
165
169
|
this.bumpSoon()
|
|
166
170
|
}
|
|
@@ -227,6 +231,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
227
231
|
await this.local.close()
|
|
228
232
|
await this.system.close()
|
|
229
233
|
await this._wakeup.close()
|
|
234
|
+
if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
|
|
230
235
|
await this._workingBee.close()
|
|
231
236
|
await this.bee.close()
|
|
232
237
|
await this.store.close()
|
|
@@ -250,7 +255,12 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
250
255
|
view: info ? info.view : EMPTY_HEAD
|
|
251
256
|
}
|
|
252
257
|
|
|
253
|
-
|
|
258
|
+
// no common ancestor (null) -> flushes -1 never matches, forcing a full diff
|
|
259
|
+
const shared = (await this.system.commonAncestor(system || EMPTY_HEAD)) || {
|
|
260
|
+
flushes: -1,
|
|
261
|
+
system: EMPTY_HEAD,
|
|
262
|
+
view: EMPTY_HEAD
|
|
263
|
+
}
|
|
254
264
|
|
|
255
265
|
return UpdateChanges.from(shared, current)
|
|
256
266
|
}
|
|
@@ -374,13 +384,18 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
374
384
|
|
|
375
385
|
// Use the view position from the system info (authoritative, post-processing)
|
|
376
386
|
// rather than from the oplog (stale, captured at append time before _bump)
|
|
377
|
-
let view = this.system.view
|
|
387
|
+
let view = this.system.view
|
|
388
|
+
if (!view) {
|
|
389
|
+
const migrated = await this.local.getUserData('autobee/migrated-view')
|
|
390
|
+
view = migrated ? encoding.decodeBootRecord(migrated) : EMPTY_HEAD
|
|
391
|
+
}
|
|
378
392
|
|
|
379
393
|
// @todo migration
|
|
380
394
|
if (result.migration) {
|
|
381
395
|
if (this.handlers.migrate) {
|
|
382
396
|
view = (await this.handlers.migrate(result.migration.views)) || EMPTY_HEAD
|
|
383
397
|
this._catchupMigratedNodes = result.migration.catchup
|
|
398
|
+
await this._storeMigratedView(view)
|
|
384
399
|
}
|
|
385
400
|
|
|
386
401
|
// ff boot invalidated by migration
|
|
@@ -476,6 +491,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
476
491
|
}
|
|
477
492
|
|
|
478
493
|
async _drain() {
|
|
494
|
+
if (this._updating) await this._updating
|
|
495
|
+
|
|
479
496
|
if (this.bootFrom) {
|
|
480
497
|
await this._initFromHead(this.bootFrom)
|
|
481
498
|
this.bootFrom = null
|
|
@@ -484,6 +501,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
484
501
|
// tracked across drain iteration
|
|
485
502
|
this.system.shared = null
|
|
486
503
|
|
|
504
|
+
this._ackRequired = false
|
|
505
|
+
|
|
487
506
|
const changes = this._hasUpdate ? new UpdateChanges(this) : null
|
|
488
507
|
if (changes) changes.track()
|
|
489
508
|
|
|
@@ -503,7 +522,12 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
503
522
|
break // revaluate conditions...
|
|
504
523
|
}
|
|
505
524
|
|
|
506
|
-
if (
|
|
525
|
+
if (await this._bumpPendingWriters()) {
|
|
526
|
+
this._needsUpdate = true
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!this._appendAck()) break
|
|
507
531
|
this._needsUpdate = true
|
|
508
532
|
}
|
|
509
533
|
|
|
@@ -518,7 +542,15 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
518
542
|
if (this._interrupting) return
|
|
519
543
|
|
|
520
544
|
if (this._needsUpdate) {
|
|
521
|
-
|
|
545
|
+
const updating = rrp()
|
|
546
|
+
this._updating = updating.promise
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
await this._update(changes)
|
|
550
|
+
} finally {
|
|
551
|
+
this._updating = null
|
|
552
|
+
updating.resolve()
|
|
553
|
+
}
|
|
522
554
|
}
|
|
523
555
|
}
|
|
524
556
|
|
|
@@ -564,58 +596,74 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
564
596
|
}
|
|
565
597
|
}
|
|
566
598
|
|
|
567
|
-
async _getOplog(key, length) {
|
|
599
|
+
async _getOplog(key, length, opts = null) {
|
|
568
600
|
const core = this.openCore(key)
|
|
569
|
-
await core.ready()
|
|
570
601
|
|
|
571
|
-
|
|
572
|
-
|
|
602
|
+
try {
|
|
603
|
+
await core.ready()
|
|
573
604
|
|
|
574
|
-
|
|
575
|
-
|
|
605
|
+
const target = length >= 0 ? length : core.length
|
|
606
|
+
if (target === 0) return null
|
|
576
607
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
608
|
+
const buf = await core.get(target - 1, opts)
|
|
609
|
+
if (buf === null) return null
|
|
580
610
|
|
|
581
|
-
|
|
611
|
+
let op = encoding.decodeOplog(buf)
|
|
582
612
|
|
|
583
|
-
|
|
613
|
+
// legacy nodes always inflate, but only indexers carry views - callers
|
|
614
|
+
// that need system info must check op.views
|
|
615
|
+
if (op.version < 3) {
|
|
616
|
+
op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
|
|
617
|
+
}
|
|
584
618
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
619
|
+
return {
|
|
620
|
+
key,
|
|
621
|
+
length: target,
|
|
622
|
+
op
|
|
623
|
+
}
|
|
624
|
+
} finally {
|
|
625
|
+
await core.close()
|
|
589
626
|
}
|
|
590
627
|
}
|
|
591
628
|
|
|
592
|
-
async _inflateLegacyOplog(buf, core, seq) {
|
|
629
|
+
async _inflateLegacyOplog(buf, core, seq, opts = null) {
|
|
593
630
|
const m = encoding.decodeRawOplog(buf)
|
|
631
|
+
|
|
632
|
+
const op = {
|
|
633
|
+
version: m.version,
|
|
634
|
+
timestamp: 0,
|
|
635
|
+
links: m.node.heads,
|
|
636
|
+
batch: { start: 0, end: m.node.batch - 1 },
|
|
637
|
+
views: null,
|
|
638
|
+
optimistic: !!m.optimistic,
|
|
639
|
+
value: m.node.value
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (m.digest === null || m.checkpoint === null) return op
|
|
643
|
+
|
|
594
644
|
const fetches = []
|
|
595
645
|
|
|
596
|
-
fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer) : buf)
|
|
597
|
-
fetches.push(
|
|
646
|
+
fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
|
|
647
|
+
fetches.push(
|
|
648
|
+
m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
|
|
649
|
+
)
|
|
598
650
|
|
|
599
651
|
const [digestNode, checkpointNode] = await Promise.all(fetches)
|
|
652
|
+
// a wait: false get we could not satisfy locally
|
|
653
|
+
if (digestNode === null || checkpointNode === null) return op
|
|
600
654
|
|
|
601
655
|
const { digest } = encoding.decodeRawOplog(digestNode)
|
|
602
656
|
const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
|
|
603
657
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
batch: { start: 0, end: m.node.batch - 1 },
|
|
609
|
-
views: {
|
|
610
|
-
system: {
|
|
611
|
-
key: digest.key,
|
|
612
|
-
length: checkpoint.system.checkpoint.length
|
|
613
|
-
},
|
|
614
|
-
flushes: seq
|
|
658
|
+
op.views = {
|
|
659
|
+
system: {
|
|
660
|
+
key: digest.key,
|
|
661
|
+
length: checkpoint.system.checkpoint.length
|
|
615
662
|
},
|
|
616
|
-
|
|
617
|
-
value: m.node.value
|
|
663
|
+
flushes: seq
|
|
618
664
|
}
|
|
665
|
+
|
|
666
|
+
return op
|
|
619
667
|
}
|
|
620
668
|
|
|
621
669
|
async _update(changes) {
|
|
@@ -744,6 +792,39 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
744
792
|
}
|
|
745
793
|
}
|
|
746
794
|
|
|
795
|
+
// append a null value node to ack writer
|
|
796
|
+
_appendAck() {
|
|
797
|
+
if (!this._ackRequired) return false
|
|
798
|
+
if (!this.writers.writable) return false
|
|
799
|
+
|
|
800
|
+
if (this.writers.localWriter.pending !== null) return false
|
|
801
|
+
|
|
802
|
+
const links = this.system.getLinks(this.local.key)
|
|
803
|
+
const t = Math.max(this._now(), this.system.timestamp)
|
|
804
|
+
|
|
805
|
+
let unlinked = false
|
|
806
|
+
for (const { key, length } of links) {
|
|
807
|
+
const hex = b4a.toString(key, 'hex')
|
|
808
|
+
const acked = this._ackedHeads.get(hex) || 0
|
|
809
|
+
if (length > acked) {
|
|
810
|
+
unlinked = true
|
|
811
|
+
break
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (!unlinked) {
|
|
815
|
+
this._ackRequired = false
|
|
816
|
+
return false
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
for (const { key, length } of links) {
|
|
820
|
+
this._ackedHeads.set(b4a.toString(key, 'hex'), length)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
this.writers.appendLocal(null, t, { start: 0, end: 0 }, links, false, null)
|
|
824
|
+
this._ackRequired = false
|
|
825
|
+
return true
|
|
826
|
+
}
|
|
827
|
+
|
|
747
828
|
async _bumpPendingWriters() {
|
|
748
829
|
if (this._catchupMigratedNodes !== null) {
|
|
749
830
|
await this._bumpMigratedWriters()
|
|
@@ -888,6 +969,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
888
969
|
if (local) {
|
|
889
970
|
this._localSystemLength = this.system.bee.context.local.length - this._localSystemStart
|
|
890
971
|
this._localViewLength = this._workingBee.context.local.length - this._localViewStart
|
|
972
|
+
this._localFlushes = this.system.flushes
|
|
891
973
|
}
|
|
892
974
|
|
|
893
975
|
this.writers.attest(witnessed)
|
|
@@ -917,11 +999,17 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
917
999
|
const boot = this.system.bootRecord()
|
|
918
1000
|
if (boot) {
|
|
919
1001
|
proms.push(this.local.setUserData('autobee/head', encoding.encodeBootRecord(boot)))
|
|
1002
|
+
if (this.system.view) proms.push(this._storeMigratedView(EMPTY_HEAD))
|
|
920
1003
|
}
|
|
921
1004
|
|
|
922
1005
|
return Promise.all(proms)
|
|
923
1006
|
}
|
|
924
1007
|
|
|
1008
|
+
_storeMigratedView(view) {
|
|
1009
|
+
const value = view.key ? encoding.encodeBootRecord(view) : null
|
|
1010
|
+
return this.local.setUserData('autobee/migrated-view', value)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
925
1013
|
static decodeValue(buf, opts) {
|
|
926
1014
|
return encoding.decodeValue(buf, opts)
|
|
927
1015
|
}
|
|
@@ -996,7 +1084,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
996
1084
|
|
|
997
1085
|
async _flushLocal() {
|
|
998
1086
|
await this.writers.flushLocal({
|
|
999
|
-
flushes: this.
|
|
1087
|
+
flushes: this._localFlushes,
|
|
1000
1088
|
system: {
|
|
1001
1089
|
key: this.system.bee.context.local.key,
|
|
1002
1090
|
start: this._localSystemStart,
|
|
@@ -1011,6 +1099,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1011
1099
|
|
|
1012
1100
|
this._localSystemStart = this.system.bee.context.local.length
|
|
1013
1101
|
this._localSystemLength = 0
|
|
1102
|
+
this._localFlushes = this.system.flushes
|
|
1014
1103
|
this._localViewStart = this._workingBee.context.local.length
|
|
1015
1104
|
this._localViewLength = 0
|
|
1016
1105
|
}
|
|
@@ -1071,23 +1160,27 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1071
1160
|
|
|
1072
1161
|
async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
|
|
1073
1162
|
const oplog = await this._getOplog(head.key, head.length)
|
|
1074
|
-
if (!oplog) return
|
|
1163
|
+
if (!oplog) return null
|
|
1075
1164
|
|
|
1076
1165
|
const verified = trusted ? await this._getOplog(trusted.key, trusted.length) : oplog
|
|
1166
|
+
if (!verified) return null
|
|
1077
1167
|
|
|
1078
|
-
|
|
1168
|
+
// legacy nodes from non-indexers have no system info to reboot from
|
|
1169
|
+
if (!oplog.op.views || !verified.op.views) return null
|
|
1079
1170
|
|
|
1080
1171
|
if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
|
|
1081
|
-
return
|
|
1172
|
+
return null
|
|
1082
1173
|
}
|
|
1083
1174
|
|
|
1084
|
-
const
|
|
1175
|
+
const tip = {
|
|
1085
1176
|
system: batchToHead(oplog.op.views.system),
|
|
1086
1177
|
verified: {
|
|
1087
1178
|
op: trusted || head,
|
|
1088
1179
|
flushes: verified.op.views.flushes
|
|
1089
1180
|
}
|
|
1090
|
-
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const moved = await this._moveTo(batchToHead(verified.op.views.system), tip)
|
|
1091
1184
|
|
|
1092
1185
|
if (moved && wait) return this.reboot.promise
|
|
1093
1186
|
|
|
@@ -1154,6 +1247,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1154
1247
|
// migrate is set when fast-forwarding from a legacy head
|
|
1155
1248
|
if (migrate) {
|
|
1156
1249
|
const view = (await this.handlers.migrate(migrate)) || EMPTY_HEAD
|
|
1250
|
+
await this._storeMigratedView(view)
|
|
1157
1251
|
this.bee.move(view)
|
|
1158
1252
|
this._workingBee.move(view)
|
|
1159
1253
|
} else {
|
|
@@ -1164,6 +1258,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1164
1258
|
this.rebootTo = null
|
|
1165
1259
|
await this.writers.refresh()
|
|
1166
1260
|
|
|
1261
|
+
await this._storeBoot()
|
|
1262
|
+
|
|
1167
1263
|
await this._update(changes)
|
|
1168
1264
|
|
|
1169
1265
|
this.emit('move-to', to, from)
|
|
@@ -1187,7 +1283,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1187
1283
|
const t = await topo.rollback(this, sys, verified)
|
|
1188
1284
|
await sys.close()
|
|
1189
1285
|
|
|
1190
|
-
await this.applyBacklog(t.tip)
|
|
1286
|
+
if (t !== null) await this.applyBacklog(t.tip)
|
|
1287
|
+
|
|
1191
1288
|
return this._update(changes)
|
|
1192
1289
|
}
|
|
1193
1290
|
|
package/lib/apply-calls.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const ID = require('hypercore-id-encoding')
|
|
2
|
+
const b4a = require('b4a')
|
|
2
3
|
|
|
3
4
|
class ApplyCalls {
|
|
4
5
|
constructor(auto) {
|
|
@@ -39,6 +40,10 @@ class ApplyCalls {
|
|
|
39
40
|
|
|
40
41
|
ackWriter(key) {
|
|
41
42
|
if (typeof key === 'string') key = ID.decode(key)
|
|
43
|
+
// acking an optimistic node means we owe it a linking node
|
|
44
|
+
if (this.applying && this.applying[0].optimistic && !b4a.equals(key, this.auto.local.key)) {
|
|
45
|
+
this.auto._ackRequired = true
|
|
46
|
+
}
|
|
42
47
|
return this.auto.system.ackWriter(key)
|
|
43
48
|
}
|
|
44
49
|
|
package/lib/system.js
CHANGED
|
@@ -2,7 +2,6 @@ const Hyperbee = require('hyperbee2')
|
|
|
2
2
|
const b4a = require('b4a')
|
|
3
3
|
|
|
4
4
|
const { AUTOBEE_VERSION } = require('./constants')
|
|
5
|
-
const asserts = require('./asserts.js')
|
|
6
5
|
const encoding = require('./encoding.js')
|
|
7
6
|
const topo = require('./topo.js')
|
|
8
7
|
|
|
@@ -13,6 +12,27 @@ const INDEXER_LT = b4a.from([3])
|
|
|
13
12
|
const EMPTY_HEAD = { length: 0, key: null }
|
|
14
13
|
const EMPTY = b4a.from([])
|
|
15
14
|
|
|
15
|
+
class SystemSnapshot {
|
|
16
|
+
constructor(bee) {
|
|
17
|
+
this.bee = bee
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async get(key, { timeout } = {}) {
|
|
21
|
+
const node = await this.bee.get(encoding.encodeSystemWriterKey(key), { timeout })
|
|
22
|
+
return node !== null ? encoding.decodeSystemWriter(node.key, node.value) : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async has(link) {
|
|
26
|
+
const node = await this.get(link.key)
|
|
27
|
+
if (!node || node.length < link.length) return false
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
close() {
|
|
32
|
+
return this.bee.close()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
module.exports = class Systembee {
|
|
17
37
|
constructor(store, name, opts = {}) {
|
|
18
38
|
this.name = name // for debuggin
|
|
@@ -96,46 +116,57 @@ module.exports = class Systembee {
|
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
async commonAncestor(head) {
|
|
99
|
-
const info = await this.getInfo()
|
|
100
|
-
|
|
101
119
|
const snap = this.bee.checkout(head)
|
|
102
120
|
|
|
103
|
-
const
|
|
104
|
-
const
|
|
121
|
+
const ca = infoIterator(this.bee)
|
|
122
|
+
const cb = infoIterator(snap)
|
|
123
|
+
|
|
124
|
+
let l = await next(ca)
|
|
125
|
+
let r = await next(cb)
|
|
126
|
+
|
|
127
|
+
while (l && r) {
|
|
128
|
+
if (l.legacy && r.legacy) {
|
|
129
|
+
// legacy nodes just compare head
|
|
130
|
+
if (sameHead(l.node.head, r.node.head)) {
|
|
131
|
+
return {
|
|
132
|
+
flushes: l.info.flushes,
|
|
133
|
+
system: l.node.head,
|
|
134
|
+
view: l.info.view || EMPTY_HEAD
|
|
135
|
+
}
|
|
136
|
+
}
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
// different legacy cores never converge - genuinely unrelated history
|
|
139
|
+
if (!b4a.equals(l.node.head.key, r.node.head.key)) return null
|
|
108
140
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
if (l.node.head.length >= r.node.head.length) l = await next(ca)
|
|
142
|
+
else r = await next(cb)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
112
145
|
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
if (l.legacy !== r.legacy) {
|
|
147
|
+
// current-format history sits above the legacy band: descend it
|
|
148
|
+
if (!l.legacy) l = await next(ca)
|
|
149
|
+
else r = await next(cb)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
if (l.flushes === r.flushes && sameHead(l.view, r.view)) {
|
|
153
|
+
if (l.info.flushes === r.info.flushes && sameHead(l.info.view, r.info.view)) {
|
|
118
154
|
return {
|
|
119
|
-
flushes: l.flushes,
|
|
120
|
-
system:
|
|
121
|
-
view: l.view || EMPTY_HEAD
|
|
155
|
+
flushes: l.info.flushes,
|
|
156
|
+
system: l.node.head,
|
|
157
|
+
view: l.info.view || EMPTY_HEAD
|
|
122
158
|
}
|
|
123
159
|
}
|
|
124
160
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
a = await hist1.next()
|
|
128
|
-
if (a.value) l = getInfo(a.value)
|
|
129
|
-
}
|
|
161
|
+
const lf = l.info.flushes
|
|
162
|
+
const rf = r.info.flushes
|
|
130
163
|
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
b = await hist2.next()
|
|
134
|
-
if (b.value) r = getInfo(b.value)
|
|
135
|
-
}
|
|
164
|
+
if (lf >= rf) l = await next(ca)
|
|
165
|
+
if (rf >= lf) r = await next(cb)
|
|
136
166
|
}
|
|
137
167
|
|
|
138
|
-
|
|
168
|
+
// no common ancestor
|
|
169
|
+
return null
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
async reset({ timeout } = {}) {
|
|
@@ -230,6 +261,14 @@ module.exports = class Systembee {
|
|
|
230
261
|
return info ? !info.isRemoved : !!optimistic
|
|
231
262
|
}
|
|
232
263
|
|
|
264
|
+
snapshot(head = null) {
|
|
265
|
+
const bee =
|
|
266
|
+
head === null
|
|
267
|
+
? this.bee.snapshot()
|
|
268
|
+
: this.bee.checkout({ length: head.length, key: head.key })
|
|
269
|
+
return new SystemSnapshot(bee)
|
|
270
|
+
}
|
|
271
|
+
|
|
233
272
|
async get(key, { unflushed = false, timeout } = {}) {
|
|
234
273
|
const node = await this.bee.get(encoding.encodeSystemWriterKey(key), { timeout })
|
|
235
274
|
const info = node !== null ? encoding.decodeSystemWriter(node.key, node.value) : null
|
|
@@ -436,6 +475,8 @@ module.exports = class Systembee {
|
|
|
436
475
|
|
|
437
476
|
w.tryPut(INFO_KEY, encoding.encodeSystemInfo(info))
|
|
438
477
|
|
|
478
|
+
this.view = info.view
|
|
479
|
+
|
|
439
480
|
await w.flush()
|
|
440
481
|
|
|
441
482
|
// Can enable if needed during debuggin
|
|
@@ -462,12 +503,16 @@ function getInfo(data) {
|
|
|
462
503
|
const prefix = k.key[0]
|
|
463
504
|
|
|
464
505
|
if (prefix === 0) {
|
|
465
|
-
|
|
506
|
+
try {
|
|
507
|
+
return encoding.decodeSystemInfo(k.value)
|
|
508
|
+
} catch {
|
|
509
|
+
return null
|
|
510
|
+
}
|
|
466
511
|
}
|
|
467
512
|
}
|
|
468
513
|
}
|
|
469
514
|
|
|
470
|
-
|
|
515
|
+
return null
|
|
471
516
|
}
|
|
472
517
|
|
|
473
518
|
function sameHead(a, b) {
|
|
@@ -475,3 +520,35 @@ function sameHead(a, b) {
|
|
|
475
520
|
if (!a.key || !b.key) return a.key === b.key
|
|
476
521
|
return b4a.equals(a.key, b.key)
|
|
477
522
|
}
|
|
523
|
+
|
|
524
|
+
async function* infoIterator(bee) {
|
|
525
|
+
try {
|
|
526
|
+
for await (const node of bee.createChangesStream()) {
|
|
527
|
+
const legacy = topo.isLegacyNode(node)
|
|
528
|
+
const info = legacy ? getLegacyInfo(node) : getInfo(node)
|
|
529
|
+
if (info) yield { node, info, legacy }
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
// fall through
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function next(it) {
|
|
537
|
+
const res = await it.next()
|
|
538
|
+
return res.done ? null : res.value
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getLegacyInfo(data) {
|
|
542
|
+
for (const { keys } of data.batch) {
|
|
543
|
+
for (const k of keys) {
|
|
544
|
+
if (b4a.equals(k.key, INFO_LEGACY_KEY)) {
|
|
545
|
+
try {
|
|
546
|
+
return encoding.decodeSystemInfo(k.value)
|
|
547
|
+
} catch {
|
|
548
|
+
return null
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return null
|
|
554
|
+
}
|
package/lib/topo.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.getMostRecentHead = getMostRecentHead
|
|
|
13
13
|
|
|
14
14
|
exports.isLinking = isLinking
|
|
15
15
|
exports.isLinkingAll = isLinkingAll
|
|
16
|
+
exports.isLegacyNode = isLegacyNode
|
|
16
17
|
|
|
17
18
|
class Clock {
|
|
18
19
|
constructor() {
|
|
@@ -41,20 +42,35 @@ async function getMostRecentHead(auto, system) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
async function rollback(auto, system, verified) {
|
|
45
|
+
if (!(await isApplied(system, verified.op))) return null
|
|
46
|
+
|
|
44
47
|
const tip = []
|
|
45
48
|
const ch = system.createChangesStream()
|
|
46
49
|
|
|
50
|
+
let found = false
|
|
51
|
+
|
|
47
52
|
for await (const data of ch) {
|
|
48
53
|
const { ref, info } = getOplog(data)
|
|
54
|
+
|
|
49
55
|
if (sameNode(ref.oplog, verified.op)) {
|
|
50
|
-
|
|
56
|
+
// stale views.flushes - the verified head describes a prefix this system
|
|
57
|
+
// does not share, so nothing above it is ours to reapply
|
|
58
|
+
if (info.flushes === verified.flushes) found = true
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// walking back, a writer's lengths only decrease - once this writer is
|
|
63
|
+
// behind the verified head the marker cannot be further down
|
|
64
|
+
if (b4a.equals(ref.oplog.key, verified.op.key) && ref.oplog.length < verified.op.length) {
|
|
51
65
|
break
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
tip.push(ref)
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
|
|
71
|
+
if (!found) return null
|
|
72
|
+
|
|
73
|
+
const inflated = await inflate(auto, tip.reverse())
|
|
58
74
|
|
|
59
75
|
// @todo: verify sort
|
|
60
76
|
|
|
@@ -65,17 +81,74 @@ async function rollback(auto, system, verified) {
|
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
// nodes we cannot resolve are returned as null rather than dropped, so callers
|
|
85
|
+
// can tell an incomplete replay from a short one
|
|
68
86
|
async function replay(auto) {
|
|
69
|
-
const ch = auto.system.bee.createChangesStream()
|
|
70
87
|
const replay = []
|
|
71
88
|
|
|
72
|
-
for await (const
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
for await (const entry of oplogIterator(auto.system.bee)) {
|
|
90
|
+
// the walk was cut short, nothing below this point is reachable
|
|
91
|
+
if (entry === null) {
|
|
92
|
+
replay.push(null)
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const ref of entry.refs) replay.push(ref)
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
// same reasoning as the iterator - never block on what we do not have
|
|
100
|
+
const inflated = await inflate(auto, replay.reverse(), { wait: false })
|
|
101
|
+
return inflated.flatMap((b) => (b === null ? [null] : b.batch))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function isApplied(system, node) {
|
|
105
|
+
const entry = await system.get(encoding.encodeSystemWriterKey(node.key))
|
|
106
|
+
if (entry === null) return false
|
|
107
|
+
|
|
108
|
+
const info = encoding.decodeSystemWriter(entry.key, entry.value)
|
|
109
|
+
return info.length >= node.length
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// tolerates legacy nodes instead of asserting
|
|
113
|
+
async function* oplogIterator(bee) {
|
|
114
|
+
try {
|
|
115
|
+
for await (const node of bee.createChangesStream({ wait: false })) {
|
|
116
|
+
const legacy = isLegacyNode(node)
|
|
117
|
+
const refs = getOplogs(node)
|
|
118
|
+
if (refs.length) yield { node, refs, legacy }
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
yield null // signal data not available
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// legacy nodes carry no isOplog flag so we cannot tell which writer update is
|
|
126
|
+
// the oplog to fetch - yield every candidate until we have a better signal
|
|
127
|
+
function getOplogs(data) {
|
|
128
|
+
const undo = data.tail || EMPTY_HEAD
|
|
129
|
+
const oplogs = []
|
|
130
|
+
const candidates = []
|
|
131
|
+
|
|
132
|
+
for (const { keys } of data.batch) {
|
|
133
|
+
for (const k of keys) {
|
|
134
|
+
if (k.key[0] !== 1) continue
|
|
135
|
+
|
|
136
|
+
let value = null
|
|
137
|
+
try {
|
|
138
|
+
value = encoding.decodeSystemWriter(k.key, k.value)
|
|
139
|
+
} catch {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// writer was just added, nothing to fetch
|
|
144
|
+
if (!value.length) continue
|
|
145
|
+
|
|
146
|
+
if (value.isOplog) oplogs.push({ undo, oplog: value })
|
|
147
|
+
else candidates.push({ undo, oplog: value })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return oplogs.length ? oplogs : candidates
|
|
79
152
|
}
|
|
80
153
|
|
|
81
154
|
async function sort(auto, batch) {
|
|
@@ -192,12 +265,19 @@ function cmp(a, b) {
|
|
|
192
265
|
return a.length < b.length ? -1 : 1
|
|
193
266
|
}
|
|
194
267
|
|
|
195
|
-
async function inflate(auto, reorder) {
|
|
268
|
+
async function inflate(auto, reorder, opts = null) {
|
|
196
269
|
const promises = []
|
|
197
270
|
const sessions = new Map()
|
|
198
271
|
|
|
199
272
|
for (let i = 0; i < reorder.length; i++) {
|
|
200
273
|
const ref = reorder[i]
|
|
274
|
+
|
|
275
|
+
// marker for a node we could not resolve, pass it through
|
|
276
|
+
if (ref === null) {
|
|
277
|
+
promises.push(null)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
201
281
|
const id = b4a.toString(ref.oplog.key, 'hex')
|
|
202
282
|
// only seeds the node - reapplication re-resolves from the witness
|
|
203
283
|
const weight = ref.oplog.weight
|
|
@@ -214,7 +294,7 @@ async function inflate(auto, reorder) {
|
|
|
214
294
|
sessions.set(id, core)
|
|
215
295
|
}
|
|
216
296
|
|
|
217
|
-
promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i))
|
|
297
|
+
promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, opts))
|
|
218
298
|
}
|
|
219
299
|
|
|
220
300
|
try {
|
|
@@ -224,9 +304,10 @@ async function inflate(auto, reorder) {
|
|
|
224
304
|
}
|
|
225
305
|
}
|
|
226
306
|
|
|
227
|
-
async function getOplogBatch(undo, core, length, weight, index) {
|
|
307
|
+
async function getOplogBatch(undo, core, length, weight, index, opts = null) {
|
|
228
308
|
const seq = length - 1
|
|
229
|
-
const block = await core.get(seq)
|
|
309
|
+
const block = await core.get(seq, opts)
|
|
310
|
+
if (block === null) return null
|
|
230
311
|
const oplog = encoding.decodeOplog(block)
|
|
231
312
|
const head = createNode(core, length, weight, oplog)
|
|
232
313
|
|
|
@@ -238,12 +319,18 @@ async function getOplogBatch(undo, core, length, weight, index) {
|
|
|
238
319
|
const end = seq + head.batch.end // skip head, we have it
|
|
239
320
|
|
|
240
321
|
for (let i = start; i < end; i++) {
|
|
241
|
-
remaining.push(core.get(i))
|
|
322
|
+
remaining.push(core.get(i, opts))
|
|
242
323
|
}
|
|
243
324
|
|
|
244
325
|
if (remaining.length) {
|
|
245
326
|
const blocks = await Promise.all(remaining)
|
|
246
327
|
for (let i = 0; i < blocks.length; i++) {
|
|
328
|
+
// keep the hole so the gap in the batch stays visible
|
|
329
|
+
if (blocks[i] === null) {
|
|
330
|
+
batch.push(null)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
|
|
247
334
|
const oplog = encoding.decodeOplog(blocks[i])
|
|
248
335
|
batch.push(createNode(core, i + start + 1, weight, oplog))
|
|
249
336
|
}
|
|
@@ -254,8 +341,12 @@ async function getOplogBatch(undo, core, length, weight, index) {
|
|
|
254
341
|
}
|
|
255
342
|
|
|
256
343
|
function getLocalBatch(undo, writer, length, weight, index) {
|
|
344
|
+
if (writer.core.closed) throw new Error('Writer core is closed')
|
|
345
|
+
|
|
257
346
|
if (length > writer.core.length) {
|
|
258
|
-
if (!writer.pending)
|
|
347
|
+
if (!writer.pending) {
|
|
348
|
+
throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}`)
|
|
349
|
+
}
|
|
259
350
|
|
|
260
351
|
let i = 0
|
|
261
352
|
while (i < writer.pending.length) {
|
package/lib/writers.js
CHANGED
|
@@ -214,7 +214,10 @@ class ActiveWriters {
|
|
|
214
214
|
return
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
const session = bootstrap.session()
|
|
218
|
+
session.setEncryption(this.auto._getEncryptionProvider())
|
|
219
|
+
|
|
220
|
+
w = new Writer(this, this.auto.system, false, session, id)
|
|
218
221
|
w.attach()
|
|
219
222
|
w.couple()
|
|
220
223
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autobee",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.11",
|
|
4
4
|
"description": "Bee based autobase",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Holepunch Inc.",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"autobee-encryption": "^0.1.2",
|
|
27
27
|
"autobee-wakeup": "^1.1.3",
|
|
28
28
|
"b4a": "^1.8.0",
|
|
29
|
-
"compact-encoding": "^
|
|
29
|
+
"compact-encoding": "^3.3.0",
|
|
30
30
|
"hyperbee2": "^2.10.3",
|
|
31
31
|
"hypercore": "^11.33.2",
|
|
32
32
|
"hypercore-id-encoding": "^1.3.0",
|