autobee 2.0.0-rc.15 → 2.0.0-rc.16
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 +48 -1
- package/encoding/spec/autobee/index.js +48 -3
- package/encoding/spec/autobee/schema.json +32 -0
- package/index.js +190 -129
- package/lib/apply-view.js +25 -0
- package/lib/fast-forward.js +168 -0
- package/lib/topo.js +1 -1
- package/lib/trusted.js +44 -0
- package/lib/writers.js +17 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,10 @@ Options:
|
|
|
78
78
|
encryptionKey: Buffer, // 32-byte key to encrypt all data at rest
|
|
79
79
|
encrypted: false, // set true if using encryptionKey
|
|
80
80
|
keyPair: { publicKey, secretKey }, // custom signing key pair for the local writer
|
|
81
|
-
optimistic: true
|
|
81
|
+
optimistic: true, // allow optimistic writes from unknown writers
|
|
82
|
+
isTrusted (key, reference) {}, // do we trust this writer, see Fast-forward
|
|
83
|
+
mostRecentTrusted (target, reference) {}, // the head we vouch for, see Fast-forward
|
|
84
|
+
fastForward: {} // see Fast-forward
|
|
82
85
|
}
|
|
83
86
|
```
|
|
84
87
|
|
|
@@ -222,6 +225,50 @@ Create an anchor node. Returns `{ key, length }`. Anchors are used to create a v
|
|
|
222
225
|
|
|
223
226
|
`true` if the system has not yet processed any nodes. Use this to bootstrap the first writer.
|
|
224
227
|
|
|
228
|
+
### Fast-forward
|
|
229
|
+
|
|
230
|
+
Fast-forward only ever deals in oplog heads — `{ key, length }` of a writer's core, never a system head.
|
|
231
|
+
|
|
232
|
+
Each flush stamps the head you vouch for into your own oplog, and peers read those stamps out of the writers they wake up on, so trust travels with the log.
|
|
233
|
+
|
|
234
|
+
#### `isTrusted(key, reference)`
|
|
235
|
+
|
|
236
|
+
Return whether `key` is a writer you trust, judged against the `reference` view.
|
|
237
|
+
|
|
238
|
+
Positive answers are cached until an undo rewinds the view, and the default is `true` unless you supply `mostRecentTrusted`, in which case it is `false`.
|
|
239
|
+
|
|
240
|
+
#### `mostRecentTrusted(target, reference)`
|
|
241
|
+
|
|
242
|
+
Return the oplog head you most recently vouched for, given the `target` view being considered.
|
|
243
|
+
|
|
244
|
+
Called at flush time to stamp your own oplog (with your view as `target` and a `null` reference), and again per candidate during discovery.
|
|
245
|
+
|
|
246
|
+
#### `fastForward.boot`
|
|
247
|
+
|
|
248
|
+
```js
|
|
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
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Pass one of `head` or `legacy`, not both.
|
|
257
|
+
|
|
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.
|
|
259
|
+
|
|
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.
|
|
261
|
+
|
|
262
|
+
#### `fastForward.conservative`
|
|
263
|
+
|
|
264
|
+
Defaults to `true`: only fast-forward onto a head a connected peer can serve whole.
|
|
265
|
+
|
|
266
|
+
The check covers the oplog head only, not the system and view cores the fast-forward then reads.
|
|
267
|
+
|
|
268
|
+
#### `await db.moveTo(head)`
|
|
269
|
+
|
|
270
|
+
Fast-forward onto an oplog head, ignoring the usual distance and conservative checks. Resolves `{ to, from }`.
|
|
271
|
+
|
|
225
272
|
### Encryption
|
|
226
273
|
|
|
227
274
|
Pass an `encryptionKey` to encrypt all writer cores and the view at rest.
|
|
@@ -856,15 +856,28 @@ const encoding29_7 = c.array(c.frame(encoding28))
|
|
|
856
856
|
// @autobee/oplog-message-v3
|
|
857
857
|
const encoding29 = {
|
|
858
858
|
preencode(state, m) {
|
|
859
|
+
let flags =
|
|
860
|
+
(m.batch ? 1 : 0) |
|
|
861
|
+
(m.views ? 2 : 0) |
|
|
862
|
+
(m.optimistic ? 8 : 0) |
|
|
863
|
+
(m.value ? 16 : 0) |
|
|
864
|
+
(m.witness ? 32 : 0) |
|
|
865
|
+
(m.attestations ? 64 : 0) |
|
|
866
|
+
(m.trusted ? 128 : 0)
|
|
867
|
+
if (m.views) {
|
|
868
|
+
flags |= m.views.view ? 4 : 0
|
|
869
|
+
}
|
|
870
|
+
|
|
859
871
|
c.uint.preencode(state, m.timestamp)
|
|
860
872
|
encoding29_1.preencode(state, m.links)
|
|
861
|
-
state
|
|
873
|
+
c.uint.preencode(state, flags)
|
|
862
874
|
|
|
863
875
|
if (m.batch) encoding24.preencode(state, m.batch)
|
|
864
876
|
if (m.views) encoding25_inline.preencode(state, m.views)
|
|
865
877
|
if (m.value) c.buffer.preencode(state, m.value)
|
|
866
878
|
if (m.witness) encoding29_6.preencode(state, m.witness)
|
|
867
879
|
if (m.attestations) encoding29_7.preencode(state, m.attestations)
|
|
880
|
+
if (m.trusted) encoding29_8.preencode(state, m.trusted)
|
|
868
881
|
},
|
|
869
882
|
encode(state, m) {
|
|
870
883
|
let flags =
|
|
@@ -873,7 +886,8 @@ const encoding29 = {
|
|
|
873
886
|
(m.optimistic ? 8 : 0) |
|
|
874
887
|
(m.value ? 16 : 0) |
|
|
875
888
|
(m.witness ? 32 : 0) |
|
|
876
|
-
(m.attestations ? 64 : 0)
|
|
889
|
+
(m.attestations ? 64 : 0) |
|
|
890
|
+
(m.trusted ? 128 : 0)
|
|
877
891
|
if (m.views) {
|
|
878
892
|
flags |= m.views.view ? 4 : 0
|
|
879
893
|
}
|
|
@@ -887,6 +901,7 @@ const encoding29 = {
|
|
|
887
901
|
if (m.value) c.buffer.encode(state, m.value)
|
|
888
902
|
if (m.witness) encoding29_6.encode(state, m.witness)
|
|
889
903
|
if (m.attestations) encoding29_7.encode(state, m.attestations)
|
|
904
|
+
if (m.trusted) encoding29_8.encode(state, m.trusted)
|
|
890
905
|
},
|
|
891
906
|
decode(state) {
|
|
892
907
|
const v = c.uint.decode(state)
|
|
@@ -903,7 +918,8 @@ const encoding29 = {
|
|
|
903
918
|
optimistic: (flags & 8) !== 0,
|
|
904
919
|
value: (flags & 16) !== 0 ? c.buffer.decode(state) : null,
|
|
905
920
|
witness: (flags & 32) !== 0 ? encoding29_6.decode(state) : null,
|
|
906
|
-
attestations: (flags & 64) !== 0 ? encoding29_7.decode(state) : null
|
|
921
|
+
attestations: (flags & 64) !== 0 ? encoding29_7.decode(state) : null,
|
|
922
|
+
trusted: (flags & 128) !== 0 ? encoding29_8.decode(state) : null
|
|
907
923
|
}
|
|
908
924
|
}
|
|
909
925
|
}
|
|
@@ -1007,12 +1023,39 @@ const encoding31 = {
|
|
|
1007
1023
|
}
|
|
1008
1024
|
}
|
|
1009
1025
|
|
|
1026
|
+
// @autobee/trusted-head
|
|
1027
|
+
const encoding32 = {
|
|
1028
|
+
preencode(state, m) {
|
|
1029
|
+
c.fixed32.preencode(state, m.key)
|
|
1030
|
+
c.uint.preencode(state, m.length)
|
|
1031
|
+
c.uint.preencode(state, m.flushes)
|
|
1032
|
+
},
|
|
1033
|
+
encode(state, m) {
|
|
1034
|
+
c.fixed32.encode(state, m.key)
|
|
1035
|
+
c.uint.encode(state, m.length)
|
|
1036
|
+
c.uint.encode(state, m.flushes)
|
|
1037
|
+
},
|
|
1038
|
+
decode(state) {
|
|
1039
|
+
const r0 = c.fixed32.decode(state)
|
|
1040
|
+
const r1 = c.uint.decode(state)
|
|
1041
|
+
const r2 = c.uint.decode(state)
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
key: r0,
|
|
1045
|
+
length: r1,
|
|
1046
|
+
flushes: r2
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1010
1051
|
// @autobee/system-info-v3.heads, deferred due to recusive use
|
|
1011
1052
|
const encoding18_3 = c.array(encoding22)
|
|
1012
1053
|
// @autobee/system-info-v3.indexers, deferred due to recusive use
|
|
1013
1054
|
const encoding18_4 = encoding18_3
|
|
1014
1055
|
// @autobee/oplog-message-v3.links, deferred due to recusive use
|
|
1015
1056
|
const encoding29_1 = encoding18_3
|
|
1057
|
+
// @autobee/oplog-message-v3.trusted, deferred due to recusive use
|
|
1058
|
+
const encoding29_8 = c.array(c.frame(encoding32))
|
|
1016
1059
|
|
|
1017
1060
|
function setVersion(v) {
|
|
1018
1061
|
version = v
|
|
@@ -1101,6 +1144,8 @@ function getEncoding(name) {
|
|
|
1101
1144
|
return encoding30
|
|
1102
1145
|
case '@autobee/manifest-data':
|
|
1103
1146
|
return encoding31
|
|
1147
|
+
case '@autobee/trusted-head':
|
|
1148
|
+
return encoding32
|
|
1104
1149
|
default:
|
|
1105
1150
|
throw new Error('Encoder not found ' + name)
|
|
1106
1151
|
}
|
|
@@ -750,6 +750,12 @@
|
|
|
750
750
|
"array": true,
|
|
751
751
|
"type": "@autobee/attestation",
|
|
752
752
|
"version": 1
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
"name": "trusted",
|
|
756
|
+
"array": true,
|
|
757
|
+
"type": "@autobee/trusted-head",
|
|
758
|
+
"version": 1
|
|
753
759
|
}
|
|
754
760
|
]
|
|
755
761
|
},
|
|
@@ -804,6 +810,32 @@
|
|
|
804
810
|
"version": 1
|
|
805
811
|
}
|
|
806
812
|
]
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"name": "trusted-head",
|
|
816
|
+
"namespace": "autobee",
|
|
817
|
+
"compact": false,
|
|
818
|
+
"flagsPosition": -1,
|
|
819
|
+
"fields": [
|
|
820
|
+
{
|
|
821
|
+
"name": "key",
|
|
822
|
+
"required": true,
|
|
823
|
+
"type": "fixed32",
|
|
824
|
+
"version": 1
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
"name": "length",
|
|
828
|
+
"required": true,
|
|
829
|
+
"type": "uint",
|
|
830
|
+
"version": 1
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
"name": "flushes",
|
|
834
|
+
"required": true,
|
|
835
|
+
"type": "uint",
|
|
836
|
+
"version": 1
|
|
837
|
+
}
|
|
838
|
+
]
|
|
807
839
|
}
|
|
808
840
|
],
|
|
809
841
|
"namespaces": [
|
package/index.js
CHANGED
|
@@ -19,11 +19,12 @@ const System = require('./lib/system.js')
|
|
|
19
19
|
const ApplyCalls = require('./lib/apply-calls.js')
|
|
20
20
|
const topo = require('./lib/topo.js')
|
|
21
21
|
const { ActiveWriters } = require('./lib/writers.js')
|
|
22
|
+
const TrustedPeers = require('./lib/trusted.js')
|
|
23
|
+
const ApplyView = require('./lib/apply-view.js')
|
|
22
24
|
const UpdateChanges = require('./lib/updates.js')
|
|
23
25
|
|
|
24
26
|
const EMPTY_HEAD = { length: 0, key: null }
|
|
25
27
|
const INTERRUPT = new Error('Apply interrupted')
|
|
26
|
-
const MIN_FF_GAP = 32
|
|
27
28
|
|
|
28
29
|
module.exports = class Autobee extends ReadyResource {
|
|
29
30
|
constructor(store, key = null, handlers = {}) {
|
|
@@ -56,7 +57,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
56
57
|
this.discoveryKey = null
|
|
57
58
|
this.id = null
|
|
58
59
|
this.bootstrap = null
|
|
59
|
-
this.
|
|
60
|
+
this._handlers = handlers
|
|
60
61
|
this.stats = { undos: 0, fastForwards: 0, drains: 0, applies: 0, appends: 0 }
|
|
61
62
|
|
|
62
63
|
this.system = new System(this.store.namespace('system'), this.name, {
|
|
@@ -66,7 +67,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
66
67
|
this.system.auto = this
|
|
67
68
|
|
|
68
69
|
this.bee = bee.snapshot()
|
|
69
|
-
this.view =
|
|
70
|
+
this.view = ApplyView.open(this.bee, this)
|
|
70
71
|
this.optimistic = handlers.optimistic !== false // TODO: should default to false instead
|
|
71
72
|
|
|
72
73
|
this.name = name // for debugging
|
|
@@ -79,18 +80,20 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
79
80
|
|
|
80
81
|
const fastForward = handlers.fastForward || {}
|
|
81
82
|
|
|
82
|
-
//
|
|
83
|
+
// oplog head to boot from: migrates or fast-forwards depending on its version
|
|
83
84
|
this.bootFrom = fastForward.boot || null
|
|
84
85
|
|
|
85
86
|
// conservative (default on): only fast-forward onto a head someone can serve whole
|
|
86
87
|
this._conservativeFF = fastForward.conservative !== false
|
|
87
88
|
|
|
89
|
+
this.trusted = new TrustedPeers(handlers)
|
|
90
|
+
|
|
88
91
|
this.ff = null
|
|
89
92
|
this.fastForwarding = null
|
|
90
93
|
this.fastForwardTo = null
|
|
91
94
|
|
|
92
95
|
this._workingBee = bee
|
|
93
|
-
this._workingView =
|
|
96
|
+
this._workingView = new ApplyView(this._workingBee, this)
|
|
94
97
|
|
|
95
98
|
this._localSystemStart = 0
|
|
96
99
|
this._localSystemLength = 0
|
|
@@ -100,6 +103,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
100
103
|
|
|
101
104
|
this._appending = []
|
|
102
105
|
this._draining = null
|
|
106
|
+
this._bootWait = null
|
|
103
107
|
|
|
104
108
|
this.legacyViews = handlers.legacyViews || []
|
|
105
109
|
|
|
@@ -107,7 +111,6 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
107
111
|
this._bootingState = null
|
|
108
112
|
this._bootingAll = null
|
|
109
113
|
|
|
110
|
-
this._handlers = handlers
|
|
111
114
|
this._now = handlers.now || Date.now // overridable for clock-drift tests
|
|
112
115
|
this._hasApply = !!handlers.apply
|
|
113
116
|
this._hasUpdate = !!handlers.update
|
|
@@ -175,6 +178,11 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
175
178
|
this.bumpSoon()
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
_requestWakeup() {
|
|
182
|
+
const session = this._wakeup._session
|
|
183
|
+
if (session) session.broadcastLookup()
|
|
184
|
+
}
|
|
185
|
+
|
|
178
186
|
_registerWakeup() {
|
|
179
187
|
if (!this.wakeupCapability) return
|
|
180
188
|
this._wakeup.recouple()
|
|
@@ -219,6 +227,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
219
227
|
|
|
220
228
|
async _close() {
|
|
221
229
|
this._interrupting = true
|
|
230
|
+
if (this._bootWait !== null) this._bootWait.resolve()
|
|
222
231
|
if (this._notifyHandler) this._notifyHandler.destroy()
|
|
223
232
|
if (this._draining) {
|
|
224
233
|
// drain may be waiting on replicated blocks
|
|
@@ -231,14 +240,14 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
231
240
|
await this._bootingAll
|
|
232
241
|
} catch {}
|
|
233
242
|
|
|
234
|
-
|
|
243
|
+
await ApplyView.close(this.view, this)
|
|
235
244
|
|
|
236
245
|
if (this.writers) await this.writers.close()
|
|
237
246
|
await this.local.close()
|
|
238
247
|
await this.system.close()
|
|
239
248
|
await this._wakeup.close()
|
|
240
249
|
if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
|
|
241
|
-
await this.
|
|
250
|
+
await this._workingView.close()
|
|
242
251
|
await this.bee.close()
|
|
243
252
|
await this.store.close()
|
|
244
253
|
}
|
|
@@ -287,6 +296,14 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
287
296
|
this._wakeup.hint(wakeup)
|
|
288
297
|
}
|
|
289
298
|
|
|
299
|
+
// the view an oplog head points at, opened and closed through the handlers
|
|
300
|
+
openViewAt(oplog) {
|
|
301
|
+
const v = oplog.op.views.view
|
|
302
|
+
if (!v) return null
|
|
303
|
+
|
|
304
|
+
return new ApplyView(this.bee.checkout({ key: v.key, length: v.start + v.length }), this)
|
|
305
|
+
}
|
|
306
|
+
|
|
290
307
|
openCore(key) {
|
|
291
308
|
const encryption = this.encryptionKey ? new WriterEncryption(this) : null
|
|
292
309
|
return this.store.get({ key, encryption })
|
|
@@ -312,7 +329,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
312
329
|
}
|
|
313
330
|
|
|
314
331
|
if (this.bootFrom) {
|
|
315
|
-
this.bootFrom = (await this.bootFrom)
|
|
332
|
+
this.bootFrom = getBootOption(await this.bootFrom)
|
|
316
333
|
}
|
|
317
334
|
}
|
|
318
335
|
|
|
@@ -400,8 +417,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
400
417
|
|
|
401
418
|
// @todo migration
|
|
402
419
|
if (result.migration) {
|
|
403
|
-
if (this.
|
|
404
|
-
view = (await this.
|
|
420
|
+
if (this._handlers.migrate) {
|
|
421
|
+
view = (await this._handlers.migrate(result.migration.views)) || EMPTY_HEAD
|
|
405
422
|
this._catchupMigratedNodes = result.migration.catchup
|
|
406
423
|
await this._storeMigratedView(view)
|
|
407
424
|
}
|
|
@@ -443,6 +460,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
443
460
|
async _bump() {
|
|
444
461
|
if (!(await this._bootReady())) return
|
|
445
462
|
|
|
463
|
+
if (this._bootWait !== null) this._bootWait.resolve()
|
|
464
|
+
|
|
446
465
|
this.bumping++
|
|
447
466
|
|
|
448
467
|
if (!this._draining) {
|
|
@@ -502,8 +521,12 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
502
521
|
this.stats.drains++
|
|
503
522
|
|
|
504
523
|
if (this.bootFrom) {
|
|
505
|
-
|
|
524
|
+
const { head = null, legacy = null, bootCondition = null } = this.bootFrom
|
|
525
|
+
|
|
506
526
|
this.bootFrom = null
|
|
527
|
+
|
|
528
|
+
if (legacy) await this._bootFromSystem(legacy)
|
|
529
|
+
else if (head) await this._bootFromHead(head, bootCondition)
|
|
507
530
|
}
|
|
508
531
|
|
|
509
532
|
// tracked across drain iteration
|
|
@@ -607,49 +630,86 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
607
630
|
}
|
|
608
631
|
|
|
609
632
|
// a scheduled fast-forward is applied by the drain before we look again
|
|
610
|
-
if (this.fastForwardTo !== null) return
|
|
611
|
-
if (this._interrupting || this.closing) return
|
|
633
|
+
if (this.fastForwardTo !== null || this.fastForwarding !== null) return
|
|
634
|
+
if (this._interrupting || this.closing || this.bootFrom) return
|
|
612
635
|
|
|
613
636
|
try {
|
|
614
|
-
await this.
|
|
637
|
+
const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
|
|
638
|
+
|
|
639
|
+
const ff = await FastForward.fromHeads(this, heads, {
|
|
640
|
+
timeout: FastForward.DEFAULT_TIMEOUT
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
if (ff !== null) await this._runFastForward(ff)
|
|
615
644
|
} catch (err) {
|
|
616
645
|
safetyCatch(err)
|
|
617
|
-
return false
|
|
618
646
|
}
|
|
647
|
+
}
|
|
619
648
|
|
|
620
|
-
|
|
649
|
+
async _readCandidateHeads(hints, timeout) {
|
|
650
|
+
const promises = []
|
|
651
|
+
|
|
652
|
+
for (const [hex, length] of hints) {
|
|
653
|
+
if (length === 0) continue
|
|
654
|
+
const key = b4a.from(hex, 'hex')
|
|
655
|
+
promises.push(this._getOplog(key, length, timeout ? { timeout } : null))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const ops = await Promise.all(promises)
|
|
659
|
+
const heads = []
|
|
660
|
+
|
|
661
|
+
for (const res of ops) {
|
|
662
|
+
if (res === null) continue
|
|
663
|
+
|
|
664
|
+
// the head we were woken on is a candidate in its own right
|
|
665
|
+
heads.push({ key: res.key, length: res.length })
|
|
666
|
+
|
|
667
|
+
if (!res.op.trusted) continue
|
|
668
|
+
|
|
669
|
+
for (const trusted of res.op.trusted) {
|
|
670
|
+
const head = this.trusted.read(trusted)
|
|
671
|
+
if (head !== null) heads.push(head)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return heads
|
|
621
676
|
}
|
|
622
677
|
|
|
623
678
|
async _getOplog(key, length, opts = null) {
|
|
624
679
|
const core = this.openCore(key)
|
|
625
680
|
|
|
626
681
|
try {
|
|
627
|
-
await
|
|
682
|
+
return await this.readOplog(core, length, opts)
|
|
683
|
+
} finally {
|
|
684
|
+
await core.close()
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// reads on a core the caller owns, so a walk does not churn a session per step
|
|
689
|
+
async readOplog(core, length, opts = null) {
|
|
690
|
+
await core.ready()
|
|
628
691
|
|
|
629
|
-
|
|
630
|
-
|
|
692
|
+
const target = length >= 0 ? length : core.length
|
|
693
|
+
if (target === 0) return null
|
|
631
694
|
|
|
632
|
-
|
|
633
|
-
|
|
695
|
+
// conservative: only proceed when a connected peer can serve the head whole
|
|
696
|
+
if (opts && opts.conservative && core.remoteContiguousLength < target) return null
|
|
634
697
|
|
|
635
|
-
|
|
636
|
-
|
|
698
|
+
const buf = await core.get(target - 1, opts)
|
|
699
|
+
if (buf === null) return null
|
|
637
700
|
|
|
638
|
-
|
|
701
|
+
let op = encoding.decodeOplog(buf)
|
|
639
702
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
703
|
+
// legacy nodes always inflate, but only indexers carry views - callers
|
|
704
|
+
// that need system info must check op.views
|
|
705
|
+
if (op.version < 3) {
|
|
706
|
+
op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
|
|
707
|
+
}
|
|
645
708
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
} finally {
|
|
652
|
-
await core.close()
|
|
709
|
+
return {
|
|
710
|
+
key: core.key,
|
|
711
|
+
length: target,
|
|
712
|
+
op
|
|
653
713
|
}
|
|
654
714
|
}
|
|
655
715
|
|
|
@@ -939,6 +999,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
939
999
|
|
|
940
1000
|
if (t.undo) {
|
|
941
1001
|
this.stats.undos++
|
|
1002
|
+
this.trusted.clear()
|
|
942
1003
|
t.view = await this.system.undo(t.undo)
|
|
943
1004
|
}
|
|
944
1005
|
|
|
@@ -989,7 +1050,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
989
1050
|
if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
|
|
990
1051
|
this.stats.applies++
|
|
991
1052
|
this._host.applying = batch
|
|
992
|
-
await this.
|
|
1053
|
+
await this._workingView.apply(userBatch)
|
|
993
1054
|
this._host.applying = null
|
|
994
1055
|
}
|
|
995
1056
|
|
|
@@ -1135,115 +1196,108 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1135
1196
|
this._localViewLength = 0
|
|
1136
1197
|
}
|
|
1137
1198
|
|
|
1138
|
-
async
|
|
1139
|
-
if (!this.
|
|
1140
|
-
if (this.
|
|
1141
|
-
if (!hints.size || this.fastForwarding || this.fastForwardTo || this.bootFrom) {
|
|
1142
|
-
return false
|
|
1143
|
-
}
|
|
1199
|
+
async moveTo(head) {
|
|
1200
|
+
if (!this.opened) await this.ready()
|
|
1201
|
+
if (this.closing) throw new Error('Autobee closed')
|
|
1144
1202
|
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
for (const [hex, length] of hints) {
|
|
1148
|
-
if (length === 0) continue
|
|
1149
|
-
const key = b4a.from(hex, 'hex')
|
|
1150
|
-
heads.push({ key, length })
|
|
1151
|
-
promises.push(this._getOplog(key, length, timeout ? { timeout } : null))
|
|
1152
|
-
}
|
|
1203
|
+
const ff = await FastForward.fromHead(this, head, null, { force: true })
|
|
1204
|
+
if (ff === null) return null
|
|
1153
1205
|
|
|
1154
|
-
|
|
1155
|
-
if (this.fastForwarding || this.fastForwardTo) return false
|
|
1206
|
+
if (!(await this._runFastForward(ff))) return null
|
|
1156
1207
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1208
|
+
return this.ff.promise
|
|
1209
|
+
}
|
|
1159
1210
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1211
|
+
// legacy: ungated boot straight onto a system head
|
|
1212
|
+
async _bootFromSystem(system) {
|
|
1213
|
+
try {
|
|
1214
|
+
const ff = new FastForward(this, system, null, { timeout: FastForward.DEFAULT_TIMEOUT })
|
|
1215
|
+
return await this._runFastForward(ff)
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
safetyCatch(err)
|
|
1218
|
+
return false
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1163
1221
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1222
|
+
// the boot head seeds the view every trust decision here is made against,
|
|
1223
|
+
// since our own view is still empty
|
|
1224
|
+
async _bootFromHead(head, bootCondition) {
|
|
1225
|
+
// just a head: one attempt, we do not wait around if it cannot be read
|
|
1226
|
+
if (bootCondition === null) {
|
|
1227
|
+
try {
|
|
1228
|
+
const ff = await FastForward.fromHead(this, head, null, {
|
|
1229
|
+
force: true,
|
|
1230
|
+
timeout: FastForward.DEFAULT_TIMEOUT
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
return ff !== null && (await this._runFastForward(ff))
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
safetyCatch(err)
|
|
1236
|
+
return false
|
|
1167
1237
|
}
|
|
1168
1238
|
}
|
|
1169
1239
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const v = best.op.views.view
|
|
1175
|
-
const view = this.bee.checkout({ key: v.key, length: v.start + v.length })
|
|
1240
|
+
// a condition needs the view at head to check against, so park until we
|
|
1241
|
+
// can read it and something acceptable turns up
|
|
1242
|
+
let opened = null
|
|
1176
1243
|
|
|
1177
|
-
let trusted = null
|
|
1178
1244
|
try {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
} finally {
|
|
1182
|
-
view.close()
|
|
1183
|
-
}
|
|
1245
|
+
while (!this._interrupting) {
|
|
1246
|
+
this._bootWait = rrp()
|
|
1184
1247
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1248
|
+
try {
|
|
1249
|
+
if (opened === null) opened = await this._bootReference(head)
|
|
1187
1250
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
...timeoutOpts
|
|
1195
|
-
})
|
|
1196
|
-
if (!oplog) return null
|
|
1197
|
-
|
|
1198
|
-
const verified = trusted
|
|
1199
|
-
? await this._getOplog(trusted.key, trusted.length, timeoutOpts)
|
|
1200
|
-
: oplog
|
|
1201
|
-
if (!verified) return null
|
|
1202
|
-
|
|
1203
|
-
// legacy nodes from non-indexers have no system info to fast-forward from
|
|
1204
|
-
if (!oplog.op.views || !verified.op.views) return null
|
|
1251
|
+
if (opened !== null && (await this._bootAttempt(head, bootCondition, opened.view))) {
|
|
1252
|
+
return true
|
|
1253
|
+
}
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
safetyCatch(err)
|
|
1256
|
+
}
|
|
1205
1257
|
|
|
1206
|
-
|
|
1207
|
-
return null
|
|
1208
|
-
}
|
|
1258
|
+
if (this._interrupting) break
|
|
1209
1259
|
|
|
1210
|
-
|
|
1211
|
-
system: batchToHead(oplog.op.views.system),
|
|
1212
|
-
verified: {
|
|
1213
|
-
op: trusted || head,
|
|
1214
|
-
flushes: verified.op.views.flushes
|
|
1260
|
+
await this._bootWait.promise
|
|
1215
1261
|
}
|
|
1262
|
+
} finally {
|
|
1263
|
+
this._bootWait = null
|
|
1264
|
+
if (opened !== null) await opened.close()
|
|
1216
1265
|
}
|
|
1217
1266
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
if (moved && wait) return this.ff.promise
|
|
1221
|
-
|
|
1222
|
-
return null
|
|
1267
|
+
return false
|
|
1223
1268
|
}
|
|
1224
1269
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1270
|
+
async _bootAttempt(head, bootCondition, reference) {
|
|
1271
|
+
// peek, so the hints are still there for the drain once we have booted
|
|
1272
|
+
const candidates = await this._readCandidateHeads(
|
|
1273
|
+
this._wakeup.hints,
|
|
1274
|
+
FastForward.DEFAULT_TIMEOUT
|
|
1275
|
+
)
|
|
1231
1276
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1277
|
+
const ff = await FastForward.fromHeads(this, [head, ...candidates], {
|
|
1278
|
+
force: true,
|
|
1279
|
+
timeout: FastForward.DEFAULT_TIMEOUT,
|
|
1280
|
+
condition: bootCondition,
|
|
1281
|
+
reference
|
|
1282
|
+
})
|
|
1234
1283
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
if (this.fastForwardTo !== null || this.fastForwarding !== null) return false
|
|
1284
|
+
return ff !== null && (await this._runFastForward(ff))
|
|
1285
|
+
}
|
|
1238
1286
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1287
|
+
async _bootReference(head) {
|
|
1288
|
+
const oplog = await FastForward.flushHead(this, head, {
|
|
1289
|
+
timeout: FastForward.DEFAULT_TIMEOUT
|
|
1290
|
+
})
|
|
1242
1291
|
|
|
1243
|
-
return
|
|
1292
|
+
return oplog === null ? null : this.openViewAt(oplog)
|
|
1244
1293
|
}
|
|
1245
1294
|
|
|
1246
1295
|
async _runFastForward(ff) {
|
|
1296
|
+
if (this.fastForwardTo !== null || this.fastForwarding !== null) {
|
|
1297
|
+
await ff.close()
|
|
1298
|
+
return false
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1247
1301
|
this.fastForwarding = ff
|
|
1248
1302
|
|
|
1249
1303
|
const result = await ff.run()
|
|
@@ -1281,7 +1335,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1281
1335
|
|
|
1282
1336
|
// migrate is set when fast-forwarding from a legacy head
|
|
1283
1337
|
if (migrate) {
|
|
1284
|
-
const view = (await this.
|
|
1338
|
+
const view = (await this._handlers.migrate(migrate)) || EMPTY_HEAD
|
|
1285
1339
|
await this._storeMigratedView(view)
|
|
1286
1340
|
this.bee.move(view)
|
|
1287
1341
|
this._workingBee.move(view)
|
|
@@ -1293,6 +1347,9 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1293
1347
|
this.fastForwardTo = null
|
|
1294
1348
|
await this.writers.refresh()
|
|
1295
1349
|
|
|
1350
|
+
// we moved, so ask our peers to tell us their heads again
|
|
1351
|
+
this._requestWakeup()
|
|
1352
|
+
|
|
1296
1353
|
await this._storeBoot()
|
|
1297
1354
|
|
|
1298
1355
|
await this._update(changes)
|
|
@@ -1333,6 +1390,17 @@ function isObject(o) {
|
|
|
1333
1390
|
return typeof o === 'object' && o && !b4a.isBuffer(o)
|
|
1334
1391
|
}
|
|
1335
1392
|
|
|
1393
|
+
function getBootOption(boot) {
|
|
1394
|
+
if (!boot) return null
|
|
1395
|
+
|
|
1396
|
+
// oldest style, supported for now but will go away: a bare key is legacy
|
|
1397
|
+
if (boot.key) return { legacy: boot, head: null, bootCondition: null }
|
|
1398
|
+
|
|
1399
|
+
asserts.assert(!(boot.head && boot.legacy), 'Boot from either a head or a legacy pointer')
|
|
1400
|
+
|
|
1401
|
+
return boot
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1336
1404
|
function noop() {}
|
|
1337
1405
|
|
|
1338
1406
|
function createAnchorCore(store, prologue, manifestData) {
|
|
@@ -1361,10 +1429,3 @@ function crashSoon(err) {
|
|
|
1361
1429
|
})
|
|
1362
1430
|
throw err
|
|
1363
1431
|
}
|
|
1364
|
-
|
|
1365
|
-
function batchToHead(b) {
|
|
1366
|
-
return {
|
|
1367
|
-
key: b.key,
|
|
1368
|
-
length: b.start + b.length
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = class ApplyView {
|
|
2
|
+
constructor(bee, auto) {
|
|
3
|
+
this.bee = bee
|
|
4
|
+
this.auto = auto
|
|
5
|
+
this.view = ApplyView.open(bee, auto)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static open(bee, auto) {
|
|
9
|
+
return auto._handlers.open ? auto._handlers.open(bee, auto) : bee
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static close(view, auto) {
|
|
13
|
+
if (!auto._handlers.close) return Promise.resolve()
|
|
14
|
+
return auto._handlers.close(view)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
apply(userBatch) {
|
|
18
|
+
return this.auto._handlers.apply(userBatch, this.view, this.auto._host)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async close() {
|
|
22
|
+
await ApplyView.close(this.view, this.auto)
|
|
23
|
+
await this.bee.close()
|
|
24
|
+
}
|
|
25
|
+
}
|
package/lib/fast-forward.js
CHANGED
|
@@ -19,6 +19,11 @@ const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
|
19
19
|
|
|
20
20
|
const EMPTY = b4a.alloc(0)
|
|
21
21
|
const DEFAULT_OP_TIMEOUT = 5_000
|
|
22
|
+
const MIN_FF_GAP = 32
|
|
23
|
+
|
|
24
|
+
// views are only stamped on the last node of a flush, so a head may sit above
|
|
25
|
+
// the most recent one - how far back we look for it
|
|
26
|
+
const MAX_FLUSH_SEARCH = 64
|
|
22
27
|
|
|
23
28
|
module.exports = class FastForward {
|
|
24
29
|
constructor(auto, head, tip, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
|
|
@@ -46,6 +51,162 @@ module.exports = class FastForward {
|
|
|
46
51
|
|
|
47
52
|
static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
|
|
48
53
|
|
|
54
|
+
static async fromHead(
|
|
55
|
+
auto,
|
|
56
|
+
head,
|
|
57
|
+
trusted,
|
|
58
|
+
{ force = false, timeout = 0, condition = null, reference = null } = {}
|
|
59
|
+
) {
|
|
60
|
+
const conservative = !force && auto._conservativeFF
|
|
61
|
+
const timeoutOpts = timeout ? { timeout } : null
|
|
62
|
+
|
|
63
|
+
const oplog = await FastForward.flushHead(auto, head, {
|
|
64
|
+
conservative,
|
|
65
|
+
...timeoutOpts
|
|
66
|
+
})
|
|
67
|
+
if (!oplog) return null
|
|
68
|
+
|
|
69
|
+
const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
|
|
70
|
+
if (!verified) return null
|
|
71
|
+
|
|
72
|
+
// legacy nodes from non-indexers have no system info to fast-forward from
|
|
73
|
+
if (!oplog.op.views || !verified.op.views) return null
|
|
74
|
+
|
|
75
|
+
if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tip = {
|
|
84
|
+
system: batchToHead(oplog.op.views.system),
|
|
85
|
+
verified: {
|
|
86
|
+
op: { key: verified.key, length: verified.length },
|
|
87
|
+
flushes: verified.op.views.flushes
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new FastForward(auto, batchToHead(verified.op.views.system), tip)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static async fromHeads(
|
|
95
|
+
auto,
|
|
96
|
+
heads,
|
|
97
|
+
{ force = false, timeout = 0, condition = null, reference = null } = {}
|
|
98
|
+
) {
|
|
99
|
+
if (reference === null) reference = auto._workingView.view
|
|
100
|
+
|
|
101
|
+
const promises = []
|
|
102
|
+
|
|
103
|
+
for (const head of heads) {
|
|
104
|
+
if (head.length === 0) continue
|
|
105
|
+
promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ops = await Promise.all(promises)
|
|
109
|
+
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
110
|
+
|
|
111
|
+
const trust = await Promise.all(
|
|
112
|
+
ops.map((res) => {
|
|
113
|
+
return res === null ? false : auto.trusted.isTrusted(res.key, reference)
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
117
|
+
|
|
118
|
+
const candidates = []
|
|
119
|
+
|
|
120
|
+
let bestTrusted = null
|
|
121
|
+
let bestTrustedFlushes = -1
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < ops.length; i++) {
|
|
124
|
+
const res = ops[i]
|
|
125
|
+
if (res === null || !res.op.views) continue
|
|
126
|
+
if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
|
|
127
|
+
|
|
128
|
+
if (!trust[i]) {
|
|
129
|
+
candidates.push(res)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (res.op.views.flushes > bestTrustedFlushes) {
|
|
134
|
+
bestTrustedFlushes = res.op.views.flushes
|
|
135
|
+
bestTrusted = res
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (bestTrusted !== null) {
|
|
140
|
+
return FastForward.fromHead(auto, bestTrusted, null, {
|
|
141
|
+
force,
|
|
142
|
+
timeout,
|
|
143
|
+
condition,
|
|
144
|
+
reference
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
|
|
149
|
+
|
|
150
|
+
for (const res of candidates) {
|
|
151
|
+
const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
|
|
152
|
+
if (trusted === null) continue
|
|
153
|
+
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
154
|
+
|
|
155
|
+
const ff = await FastForward.fromHead(auto, res, trusted, {
|
|
156
|
+
force,
|
|
157
|
+
timeout,
|
|
158
|
+
condition,
|
|
159
|
+
reference
|
|
160
|
+
})
|
|
161
|
+
if (ff !== null) return ff
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static async flushHead(auto, head, opts) {
|
|
168
|
+
const core = auto.openCore(head.key)
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
let length = head.length
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < MAX_FLUSH_SEARCH; i++) {
|
|
174
|
+
const oplog = await auto.readOplog(core, length, opts)
|
|
175
|
+
if (oplog === null) return null
|
|
176
|
+
if (oplog.op.views) return oplog
|
|
177
|
+
|
|
178
|
+
length = oplog.length - 1
|
|
179
|
+
if (length <= 0) return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null
|
|
183
|
+
} finally {
|
|
184
|
+
await core.close()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static async accepts(auto, oplog, condition, reference) {
|
|
189
|
+
const opened = auto.openViewAt(oplog)
|
|
190
|
+
if (opened === null) return false
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
return !!(await condition(opened.view, reference))
|
|
194
|
+
} finally {
|
|
195
|
+
await opened.close()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static async mostRecentTrusted(auto, head, reference) {
|
|
200
|
+
const opened = auto.openViewAt(head)
|
|
201
|
+
if (opened === null) return null
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
return (await auto.trusted.mostRecentTrusted(opened.view, reference)) || null
|
|
205
|
+
} finally {
|
|
206
|
+
await opened.close()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
49
210
|
async run() {
|
|
50
211
|
try {
|
|
51
212
|
if (!this.running) this.running = this._run()
|
|
@@ -228,6 +389,13 @@ module.exports = class FastForward {
|
|
|
228
389
|
}
|
|
229
390
|
}
|
|
230
391
|
|
|
392
|
+
function batchToHead(b) {
|
|
393
|
+
return {
|
|
394
|
+
key: b.key,
|
|
395
|
+
length: b.start + b.length
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
231
399
|
function decodeLegacySystemInfo(buffer) {
|
|
232
400
|
const state = { start: 0, end: buffer.length, buffer }
|
|
233
401
|
const version = c.uint.decode(state)
|
package/lib/topo.js
CHANGED
|
@@ -308,7 +308,7 @@ function getLocalBatch(undo, writer, length, weight, index) {
|
|
|
308
308
|
|
|
309
309
|
if (length > writer.core.length) {
|
|
310
310
|
if (!writer.pending) {
|
|
311
|
-
throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}`)
|
|
311
|
+
throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}@${writer.core.id}`)
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
let i = 0
|
package/lib/trusted.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const b4a = require('b4a')
|
|
2
|
+
|
|
3
|
+
class TrustedPeers {
|
|
4
|
+
constructor(hooks) {
|
|
5
|
+
this.trusted = new Map()
|
|
6
|
+
this.pending = new Map()
|
|
7
|
+
this.hooks = hooks
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async isTrusted(key, reference) {
|
|
11
|
+
const hex = b4a.toString(key, 'hex')
|
|
12
|
+
if (this.trusted.get(hex)) return true
|
|
13
|
+
|
|
14
|
+
let promise = this.pending.get(hex)
|
|
15
|
+
if (promise) return promise
|
|
16
|
+
|
|
17
|
+
promise = this.hooks.isTrusted
|
|
18
|
+
? this.hooks.isTrusted(key, reference)
|
|
19
|
+
: Promise.resolve(!this.hooks.mostRecentTrusted)
|
|
20
|
+
this.pending.set(hex, promise)
|
|
21
|
+
|
|
22
|
+
const trusted = await promise
|
|
23
|
+
if (trusted) this.trusted.set(hex, trusted)
|
|
24
|
+
|
|
25
|
+
if (this.pending.get(hex) === promise) this.pending.delete(hex)
|
|
26
|
+
return trusted
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
read(trusted) {
|
|
30
|
+
return trusted || null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
mostRecentTrusted(target, reference) {
|
|
34
|
+
if (!this.hooks.mostRecentTrusted) return Promise.resolve(null)
|
|
35
|
+
return this.hooks.mostRecentTrusted(target, reference)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clear() {
|
|
39
|
+
this.pending.clear()
|
|
40
|
+
this.trusted.clear()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = TrustedPeers
|
package/lib/writers.js
CHANGED
|
@@ -64,6 +64,9 @@ class ActiveWriters {
|
|
|
64
64
|
w = await this.add(key)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// prefetch the oplog head for the fast-forward that runs right after us
|
|
68
|
+
if (w && length > 0) w.core.get(length - 1).catch(noop)
|
|
69
|
+
|
|
67
70
|
// TODO: signal that we've seen length on w for gc
|
|
68
71
|
|
|
69
72
|
this.bump()
|
|
@@ -95,16 +98,18 @@ class ActiveWriters {
|
|
|
95
98
|
return this.localWriter.clear()
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
flushLocal(views) {
|
|
101
|
+
async flushLocal(views) {
|
|
99
102
|
const w = this.localWriter
|
|
100
103
|
|
|
101
104
|
// only consume the queue when a flush will actually write
|
|
102
|
-
if (w.pending === null || w.processed === 0) return
|
|
105
|
+
if (w.pending === null || w.processed === 0) return
|
|
103
106
|
|
|
104
107
|
const queued = this.attestations
|
|
105
108
|
this.attestations = []
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
const trusted = await this.auto.trusted.mostRecentTrusted(this.auto._workingView.view, null)
|
|
111
|
+
|
|
112
|
+
return w.flush(views, queued, trusted)
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
async refresh() {
|
|
@@ -623,7 +628,8 @@ class Writer {
|
|
|
623
628
|
batch,
|
|
624
629
|
views: null,
|
|
625
630
|
optimistic: optimistic && !this.writers.writable,
|
|
626
|
-
value
|
|
631
|
+
value,
|
|
632
|
+
trusted: null
|
|
627
633
|
}
|
|
628
634
|
|
|
629
635
|
const node = createNode(this.core, this.appendLength++, -1, oplog)
|
|
@@ -646,12 +652,16 @@ class Writer {
|
|
|
646
652
|
this.processed = 0
|
|
647
653
|
}
|
|
648
654
|
|
|
649
|
-
flush(views, queued) {
|
|
655
|
+
flush(views, queued, trusted) {
|
|
650
656
|
if (this.pending === null || this.processed === 0) return Promise.resolve()
|
|
651
657
|
|
|
652
658
|
const head = this.pending[this.processed - 1]
|
|
653
659
|
head.views = views
|
|
654
660
|
|
|
661
|
+
head.trusted = trusted
|
|
662
|
+
? [{ key: trusted.key, length: trusted.length, flushes: views.flushes }]
|
|
663
|
+
: null
|
|
664
|
+
|
|
655
665
|
if (queued && queued.length) {
|
|
656
666
|
const attestations = []
|
|
657
667
|
for (const { key, weight } of queued) {
|
|
@@ -695,7 +705,8 @@ function createNode(core, length, weight, oplog) {
|
|
|
695
705
|
batch: oplog.batch,
|
|
696
706
|
views: oplog.views,
|
|
697
707
|
optimistic: oplog.optimistic,
|
|
698
|
-
value: oplog.value
|
|
708
|
+
value: oplog.value,
|
|
709
|
+
trusted: oplog.trusted || []
|
|
699
710
|
}
|
|
700
711
|
}
|
|
701
712
|
|