autobee 2.0.0-rc.22 → 2.0.0-rc.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +23 -47
- package/lib/boot.js +1 -192
- package/lib/fast-forward.js +61 -123
- package/lib/migrations.js +421 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const { ActiveWriters } = require('./lib/writers.js')
|
|
|
22
22
|
const TrustedPeers = require('./lib/trusted.js')
|
|
23
23
|
const ApplyView = require('./lib/apply-view.js')
|
|
24
24
|
const UpdateChanges = require('./lib/updates.js')
|
|
25
|
+
const migrations = require('./lib/migrations.js')
|
|
25
26
|
|
|
26
27
|
const EMPTY_HEAD = { length: 0, key: null }
|
|
27
28
|
const INTERRUPT = new Error('Apply interrupted')
|
|
@@ -78,13 +79,16 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
78
79
|
this.writers = null
|
|
79
80
|
this.bumping = 0
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
// fastForward: false disables all fast-forwards - both wakeup and boot
|
|
83
|
+
const fastForward = handlers.fastForward === false ? null : handlers.fastForward || {}
|
|
84
|
+
|
|
85
|
+
this._ffEnabled = fastForward !== null
|
|
82
86
|
|
|
83
87
|
// oplog head to boot from: migrates or fast-forwards depending on its version
|
|
84
|
-
this.bootFrom = fastForward.boot || null
|
|
88
|
+
this.bootFrom = (fastForward && fastForward.boot) || null
|
|
85
89
|
|
|
86
90
|
// conservative (default on): only fast-forward onto a head someone can serve whole
|
|
87
|
-
this._conservativeFF = fastForward.conservative !== false
|
|
91
|
+
this._conservativeFF = !fastForward || fastForward.conservative !== false
|
|
88
92
|
|
|
89
93
|
this.trusted = new TrustedPeers(handlers)
|
|
90
94
|
|
|
@@ -112,6 +116,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
112
116
|
this._bootingAll = null
|
|
113
117
|
|
|
114
118
|
this._now = handlers.now || Date.now // overridable for clock-drift tests
|
|
119
|
+
this._preapply = handlers.preapply || null
|
|
120
|
+
this._preApplied = false
|
|
115
121
|
this._hasApply = !!handlers.apply
|
|
116
122
|
this._hasUpdate = !!handlers.update
|
|
117
123
|
this._needsUpdate = false
|
|
@@ -504,9 +510,20 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
504
510
|
this.emit('error', err)
|
|
505
511
|
}
|
|
506
512
|
|
|
513
|
+
// one-shot user gate: nothing applies until the host has resolved whatever
|
|
514
|
+
// state apply depends on (e.g. legacy views recorded by a migration)
|
|
515
|
+
async _runPreApply() {
|
|
516
|
+
if (this._preapply === null || this._preApplied) return
|
|
517
|
+
|
|
518
|
+
this._preApplied = true
|
|
519
|
+
await this._preapply(this.view)
|
|
520
|
+
}
|
|
521
|
+
|
|
507
522
|
async _drain() {
|
|
508
523
|
if (this._updating) await this._updating
|
|
509
524
|
|
|
525
|
+
await this._runPreApply()
|
|
526
|
+
|
|
510
527
|
this.stats.drains++
|
|
511
528
|
|
|
512
529
|
if (this.bootFrom) {
|
|
@@ -626,6 +643,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
626
643
|
const hints = await this._applyWakeupHints()
|
|
627
644
|
if (!hints.size) return
|
|
628
645
|
|
|
646
|
+
if (!this._ffEnabled) return
|
|
647
|
+
|
|
629
648
|
// a scheduled fast-forward is applied by the drain before we look again
|
|
630
649
|
if (this.fastForwardTo !== null || this.fastForwarding !== null) return
|
|
631
650
|
if (this._interrupting || this.closing || this.bootFrom) return
|
|
@@ -700,7 +719,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
700
719
|
// legacy nodes always inflate, but only indexers carry views - callers
|
|
701
720
|
// that need system info must check op.views
|
|
702
721
|
if (op.version < 3) {
|
|
703
|
-
op = await
|
|
722
|
+
op = await migrations.inflateLegacyOplog(buf, core, target - 1, opts)
|
|
704
723
|
}
|
|
705
724
|
|
|
706
725
|
return {
|
|
@@ -710,49 +729,6 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
710
729
|
}
|
|
711
730
|
}
|
|
712
731
|
|
|
713
|
-
async _inflateLegacyOplog(buf, core, seq, opts = null) {
|
|
714
|
-
const m = encoding.decodeRawOplog(buf)
|
|
715
|
-
|
|
716
|
-
const op = {
|
|
717
|
-
version: m.version,
|
|
718
|
-
timestamp: 0,
|
|
719
|
-
links: m.node.heads,
|
|
720
|
-
batch: { start: 0, end: m.node.batch - 1 },
|
|
721
|
-
views: null,
|
|
722
|
-
optimistic: !!m.optimistic,
|
|
723
|
-
value: m.node.value
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
|
|
727
|
-
|
|
728
|
-
const fetches = []
|
|
729
|
-
|
|
730
|
-
fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
|
|
731
|
-
fetches.push(
|
|
732
|
-
m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
const [digestNode, checkpointNode] = await Promise.all(fetches)
|
|
736
|
-
// a wait: false get we could not satisfy locally
|
|
737
|
-
if (digestNode === null || checkpointNode === null) return op
|
|
738
|
-
|
|
739
|
-
const { digest } = encoding.decodeRawOplog(digestNode)
|
|
740
|
-
const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
|
|
741
|
-
|
|
742
|
-
if (!checkpoint.system || !checkpoint.system.checkpoint) return op
|
|
743
|
-
|
|
744
|
-
op.views = {
|
|
745
|
-
system: {
|
|
746
|
-
key: digest.key,
|
|
747
|
-
start: 0,
|
|
748
|
-
length: checkpoint.system.checkpoint.length
|
|
749
|
-
},
|
|
750
|
-
flushes: seq
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return op
|
|
754
|
-
}
|
|
755
|
-
|
|
756
732
|
async _update(changes) {
|
|
757
733
|
this._needsUpdate = false
|
|
758
734
|
this.bee.update(this._workingBee.root)
|
package/lib/boot.js
CHANGED
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
const c = require('compact-encoding')
|
|
2
|
-
const b4a = require('b4a')
|
|
3
|
-
const crypto = require('hypercore-crypto')
|
|
4
|
-
const { AutobeeEncryption } = require('autobee-encryption')
|
|
5
|
-
const { decodeBlock } = require('hyperbee2/lib/encoding.js')
|
|
6
|
-
|
|
7
1
|
const encoding = require('./encoding.js')
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
const { getEncoding } = require('../encoding/spec/autobee')
|
|
11
|
-
|
|
12
|
-
const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
|
|
13
|
-
const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
|
|
14
|
-
|
|
15
|
-
const EMPTY = b4a.alloc(0)
|
|
16
|
-
const INDEX_VERSION = 1
|
|
17
|
-
const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
2
|
+
const { checkAutobaseMigration } = require('./migrations.js')
|
|
18
3
|
|
|
19
4
|
module.exports = async function boot(
|
|
20
5
|
corestore,
|
|
@@ -153,64 +138,6 @@ module.exports = async function boot(
|
|
|
153
138
|
return result
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
|
|
157
|
-
const bootRecord = await local.getUserData('autobase/boot')
|
|
158
|
-
if (!bootRecord) return null
|
|
159
|
-
|
|
160
|
-
const { key } = encoding.decodeAutobaseBootRecord(bootRecord)
|
|
161
|
-
const core = store.get({ key: key, encryption: null })
|
|
162
|
-
await core.ready()
|
|
163
|
-
|
|
164
|
-
const system = { key, length: core.length }
|
|
165
|
-
|
|
166
|
-
// setup encryption
|
|
167
|
-
const encryptionKey = await local.getUserData('autobase/encryption')
|
|
168
|
-
await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
|
|
169
|
-
|
|
170
|
-
const catchup = await getCatchupHeads(core)
|
|
171
|
-
const nodes = await Promise.all(
|
|
172
|
-
catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
// Decode hyperbee block using hyperbee2 compat
|
|
176
|
-
const node = decodeBlock(await core.get(core.length - 1))
|
|
177
|
-
assert(node.keys.length > 0, 'bad system block')
|
|
178
|
-
|
|
179
|
-
// Decode system info from hyperbee block using autobee compat
|
|
180
|
-
const info = decodeLegacySystemInfo(node.keys[0].value)
|
|
181
|
-
|
|
182
|
-
const indexerManifests = await Promise.all(
|
|
183
|
-
info.indexers.map((idx) => getCoreManifest(store, idx.key))
|
|
184
|
-
)
|
|
185
|
-
const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
|
|
186
|
-
const entropy =
|
|
187
|
-
info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
|
|
188
|
-
|
|
189
|
-
const mappedViews = new Map()
|
|
190
|
-
for (const name of legacyViews) {
|
|
191
|
-
const v = await findViewByName(
|
|
192
|
-
store,
|
|
193
|
-
bootstrap.key,
|
|
194
|
-
encryptionKey,
|
|
195
|
-
indexerManifests,
|
|
196
|
-
views,
|
|
197
|
-
entropy,
|
|
198
|
-
name
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
mappedViews.set(name, v)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
await core.close()
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
encryptionKey,
|
|
208
|
-
system,
|
|
209
|
-
views: mappedViews,
|
|
210
|
-
catchup: nodes
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
141
|
async function getLocalKey(bootstrap) {
|
|
215
142
|
const [legacy, current] = await Promise.all([
|
|
216
143
|
bootstrap.getUserData('autobase/local'),
|
|
@@ -220,121 +147,3 @@ async function getLocalKey(bootstrap) {
|
|
|
220
147
|
if (current) return current
|
|
221
148
|
return legacy
|
|
222
149
|
}
|
|
223
|
-
|
|
224
|
-
async function getPersistedView(store, view) {
|
|
225
|
-
const core = store.get(view.key)
|
|
226
|
-
await core.ready()
|
|
227
|
-
|
|
228
|
-
const length = core.length
|
|
229
|
-
await core.close()
|
|
230
|
-
|
|
231
|
-
return { key: view.key, length }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function getCatchupHeads(core) {
|
|
235
|
-
const session = core.session({ name: 'batch' })
|
|
236
|
-
await session.ready()
|
|
237
|
-
|
|
238
|
-
const nodes = []
|
|
239
|
-
for (let i = core.length; i < session.length; i++) {
|
|
240
|
-
const node = await session.get(i, { wait: false })
|
|
241
|
-
if (!node) throw new Error('Expect nodes to exist locally')
|
|
242
|
-
|
|
243
|
-
const entry = decodeBlock(node).keys[0]
|
|
244
|
-
if (entry.key[0] !== 0x01) continue
|
|
245
|
-
|
|
246
|
-
const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
|
|
247
|
-
if (!length) continue
|
|
248
|
-
nodes.push({ key, length })
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return nodes
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
|
|
255
|
-
const batch = []
|
|
256
|
-
|
|
257
|
-
const core = store.get(head.key)
|
|
258
|
-
await core.ready()
|
|
259
|
-
|
|
260
|
-
if (encryptionKey) {
|
|
261
|
-
await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
let seq = head.length - 1
|
|
265
|
-
const block = await core.get(seq--, { wait: false })
|
|
266
|
-
if (!block) throw new Error('Expect writer node to exist locally')
|
|
267
|
-
|
|
268
|
-
const node = encoding.decodeOplog(block)
|
|
269
|
-
|
|
270
|
-
batch.unshift({ ...head, ...node, from: core })
|
|
271
|
-
|
|
272
|
-
while (seq >= 0) {
|
|
273
|
-
const block = await core.get(seq--, { wait: false })
|
|
274
|
-
if (!block) break
|
|
275
|
-
|
|
276
|
-
const node = encoding.decodeOplog(block)
|
|
277
|
-
if (!node.batch.end) break
|
|
278
|
-
|
|
279
|
-
batch.unshift({ key: head.key, length: seq + 2, from: core, ...node })
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return batch
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function getCoreManifest(store, key) {
|
|
286
|
-
const core = store.get(key)
|
|
287
|
-
await core.ready()
|
|
288
|
-
|
|
289
|
-
const manifest = core.manifest
|
|
290
|
-
await core.close()
|
|
291
|
-
|
|
292
|
-
return manifest
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function decodeLegacySystemInfo(buffer) {
|
|
296
|
-
const state = { start: 0, end: buffer.length, buffer }
|
|
297
|
-
const version = c.uint.decode(state)
|
|
298
|
-
|
|
299
|
-
state.start--
|
|
300
|
-
|
|
301
|
-
switch (version) {
|
|
302
|
-
case 1:
|
|
303
|
-
return SystemInfoV1.decode(state)
|
|
304
|
-
case 2:
|
|
305
|
-
return SystemInfoV2.decode(state)
|
|
306
|
-
default:
|
|
307
|
-
bail('Expected legacy system info')
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
|
|
312
|
-
const encryptionId = crypto.hash(encryptionKey || EMPTY)
|
|
313
|
-
const version = c.encode(c.uint, INDEX_VERSION)
|
|
314
|
-
|
|
315
|
-
return crypto.hash([
|
|
316
|
-
NS_SIGNER_NAMESPACE,
|
|
317
|
-
version,
|
|
318
|
-
bootstrap,
|
|
319
|
-
encryptionId,
|
|
320
|
-
entropy,
|
|
321
|
-
b4a.from(name)
|
|
322
|
-
])
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function findViewByName(store, key, encryptionKey, indexerManifests, views, entropy, name) {
|
|
326
|
-
if (indexerManifests.length === 0) return null
|
|
327
|
-
|
|
328
|
-
const namespace = deriveNamespace(name, key, entropy, encryptionKey)
|
|
329
|
-
|
|
330
|
-
for (const v of views) {
|
|
331
|
-
const manifest = await getCoreManifest(store, v.key)
|
|
332
|
-
if (manifest.signers.length === 0) continue
|
|
333
|
-
|
|
334
|
-
const signer = manifest.signers[0]
|
|
335
|
-
|
|
336
|
-
if (b4a.equals(signer.namespace, namespace)) return v
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return null
|
|
340
|
-
}
|
package/lib/fast-forward.js
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
const safetyCatch = require('safety-catch')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
3
|
const Hypercore = require('hypercore')
|
|
4
|
-
const c = require('compact-encoding')
|
|
5
|
-
const crypto = require('hypercore-crypto')
|
|
6
|
-
|
|
7
|
-
const { decodeBlock } = require('hyperbee2/lib/encoding.js')
|
|
8
4
|
|
|
9
5
|
const System = require('./system.js')
|
|
10
|
-
const
|
|
6
|
+
const migrations = require('./migrations.js')
|
|
11
7
|
const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
|
|
12
|
-
const { getEncoding } = require('../encoding/spec/autobee')
|
|
13
|
-
|
|
14
|
-
const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
|
|
15
|
-
const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
|
|
16
8
|
|
|
17
|
-
const INDEX_VERSION = 1
|
|
18
|
-
const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
19
|
-
|
|
20
|
-
const EMPTY = b4a.alloc(0)
|
|
21
9
|
const DEFAULT_OP_TIMEOUT = 5_000
|
|
22
10
|
const MIN_FF_GAP = 32
|
|
23
11
|
|
|
@@ -243,7 +231,25 @@ module.exports = class FastForward {
|
|
|
243
231
|
return null
|
|
244
232
|
}
|
|
245
233
|
|
|
246
|
-
|
|
234
|
+
const result = await this._migrate()
|
|
235
|
+
if (result === null || result.autobee === null) return result
|
|
236
|
+
|
|
237
|
+
// an indexer of the newest legacy generation already moved to autobee -
|
|
238
|
+
// fast-forward straight onto its announced head instead of migrating,
|
|
239
|
+
// falling back to the legacy migration if the head cannot be booted
|
|
240
|
+
try {
|
|
241
|
+
this.head = result.autobee
|
|
242
|
+
await this.system.boot(this.head, { timeout: this.timeout })
|
|
243
|
+
if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
|
|
244
|
+
throw new Error('Expected an autobee system')
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
safetyCatch(err)
|
|
248
|
+
this.head = result.head
|
|
249
|
+
return { head: result.head, tip: null, migrate: result.migrate }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// fall through to the plain fast-forward below
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
const promises = []
|
|
@@ -280,47 +286,58 @@ module.exports = class FastForward {
|
|
|
280
286
|
// Upgrade a legacy (pre-AUTOBEE_VERSION) system in place. The system already
|
|
281
287
|
// booted above (coerced to the current struct), but the original v1/v2 struct
|
|
282
288
|
// is needed to recover the indexers/views/entropy, so decode the raw block.
|
|
289
|
+
//
|
|
290
|
+
// A head may be arbitrarily old: an indexer rotation freezes the system core
|
|
291
|
+
// and moves the indexers to a successor. The frozen core cannot reveal its
|
|
292
|
+
// successor (the promotion only lands in its unsigned tail), but the
|
|
293
|
+
// indexers' newest oplog digests stamp the newest system they know - chase
|
|
294
|
+
// those so the migration lands on the newest generation instead of the head
|
|
295
|
+
// we were given.
|
|
283
296
|
async _migrate() {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
await core.ready()
|
|
297
|
+
let head = { key: this.head.key, length: this.head.length }
|
|
298
|
+
const seen = new Set()
|
|
287
299
|
|
|
288
|
-
|
|
289
|
-
|
|
300
|
+
for (let hops = 0; hops < migrations.MAX_MIGRATE_HOPS; hops++) {
|
|
301
|
+
seen.add(b4a.toString(head.key, 'hex'))
|
|
290
302
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
await core.
|
|
303
|
+
const core = this.auto.store.get({ key: head.key, encryption: null })
|
|
304
|
+
this.cores.push(core)
|
|
305
|
+
await core.ready()
|
|
306
|
+
|
|
307
|
+
// setup encryption
|
|
308
|
+
await core.setEncryption(this.auto.getSystemEncryption())
|
|
309
|
+
|
|
310
|
+
// successors are resolved by key only - wait for the replicated length
|
|
311
|
+
if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
|
|
312
|
+
if (!head.length || this.destroyed) return null
|
|
313
|
+
|
|
314
|
+
const info = await migrations.readLegacySystemInfo(core, head.length, {
|
|
294
315
|
timeout: this.timeout,
|
|
295
316
|
activeRequests: this.activeRequests
|
|
296
317
|
})
|
|
297
|
-
)
|
|
298
|
-
assert(node.keys.length > 0, 'bad system block')
|
|
299
|
-
|
|
300
|
-
// Decode system info from hyperbee block using autobee compat
|
|
301
|
-
const info = decodeLegacySystemInfo(node.keys[0].value)
|
|
302
318
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
)
|
|
306
|
-
const entropy =
|
|
307
|
-
info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
|
|
319
|
+
const { legacy, autobee } = await migrations.findNewestSystem(this, info)
|
|
320
|
+
if (this.destroyed) return null
|
|
308
321
|
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
if (legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
|
|
323
|
+
head = { key: legacy, length: 0 }
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
311
326
|
|
|
312
|
-
|
|
313
|
-
viewProms.push(this.findViewByName(views, indexerManifests, info.views, entropy, name))
|
|
314
|
-
}
|
|
327
|
+
this.head = head
|
|
315
328
|
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
const views = await migrations.resolveLegacyViews(this, info)
|
|
330
|
+
if (views === null || this.destroyed) return null
|
|
318
331
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
332
|
+
return {
|
|
333
|
+
head,
|
|
334
|
+
tip: null,
|
|
335
|
+
migrate: views,
|
|
336
|
+
autobee: autobee === null ? null : batchToHead(autobee.op.views.system)
|
|
337
|
+
}
|
|
323
338
|
}
|
|
339
|
+
|
|
340
|
+
return null
|
|
324
341
|
}
|
|
325
342
|
|
|
326
343
|
async _resolveLength() {
|
|
@@ -329,47 +346,7 @@ module.exports = class FastForward {
|
|
|
329
346
|
|
|
330
347
|
await core.ready()
|
|
331
348
|
|
|
332
|
-
return coreLength(core, this.timeout)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async getCoreManifest(key, length) {
|
|
336
|
-
const core = this.auto.store.get(key)
|
|
337
|
-
this.cores.push(core)
|
|
338
|
-
|
|
339
|
-
await core.ready()
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
if (!core.manifest) {
|
|
343
|
-
await core.get(length - 1, {
|
|
344
|
-
timeout: this.timeout,
|
|
345
|
-
activeRequests: this.activeRequests
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
} catch {
|
|
349
|
-
return null
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return core.manifest
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async findViewByName(result, indexerManifests, views, entropy, name) {
|
|
356
|
-
if (indexerManifests.length === 0) return null
|
|
357
|
-
|
|
358
|
-
const namespace = deriveNamespace(name, this.auto.key, entropy, this.auto.encryptionKey)
|
|
359
|
-
|
|
360
|
-
for (const v of views) {
|
|
361
|
-
const manifest = await this.getCoreManifest(v.key, v.length)
|
|
362
|
-
if (!manifest) continue
|
|
363
|
-
|
|
364
|
-
if (manifest.signers.length === 0) continue
|
|
365
|
-
|
|
366
|
-
const signer = manifest.signers[0]
|
|
367
|
-
|
|
368
|
-
if (b4a.equals(signer.namespace, namespace)) {
|
|
369
|
-
result.set(name, v)
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
}
|
|
349
|
+
return migrations.coreLength(core, this.timeout)
|
|
373
350
|
}
|
|
374
351
|
|
|
375
352
|
// cancels every read in flight and fails any that come after
|
|
@@ -391,42 +368,3 @@ function batchToHead(b) {
|
|
|
391
368
|
length: b.start + b.length
|
|
392
369
|
}
|
|
393
370
|
}
|
|
394
|
-
|
|
395
|
-
function decodeLegacySystemInfo(buffer) {
|
|
396
|
-
const state = { start: 0, end: buffer.length, buffer }
|
|
397
|
-
const version = c.uint.decode(state)
|
|
398
|
-
|
|
399
|
-
state.start--
|
|
400
|
-
|
|
401
|
-
switch (version) {
|
|
402
|
-
case 1:
|
|
403
|
-
return SystemInfoV1.decode(state)
|
|
404
|
-
case 2:
|
|
405
|
-
return SystemInfoV2.decode(state)
|
|
406
|
-
default:
|
|
407
|
-
return { version }
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function coreLength(core, timeout) {
|
|
412
|
-
if (core.length) return core.length
|
|
413
|
-
|
|
414
|
-
return new Promise((resolve) => {
|
|
415
|
-
core.on('append', () => resolve(core.length))
|
|
416
|
-
setTimeout(resolve, timeout, 0)
|
|
417
|
-
})
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
|
|
421
|
-
const encryptionId = crypto.hash(encryptionKey || EMPTY)
|
|
422
|
-
const version = c.encode(c.uint, INDEX_VERSION)
|
|
423
|
-
|
|
424
|
-
return crypto.hash([
|
|
425
|
-
NS_SIGNER_NAMESPACE,
|
|
426
|
-
version,
|
|
427
|
-
bootstrap,
|
|
428
|
-
encryptionId,
|
|
429
|
-
entropy,
|
|
430
|
-
b4a.from(name)
|
|
431
|
-
])
|
|
432
|
-
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
const c = require('compact-encoding')
|
|
2
|
+
const b4a = require('b4a')
|
|
3
|
+
const crypto = require('hypercore-crypto')
|
|
4
|
+
const { AutobeeEncryption } = require('autobee-encryption')
|
|
5
|
+
const { decodeBlock } = require('hyperbee2/lib/encoding.js')
|
|
6
|
+
|
|
7
|
+
const encoding = require('./encoding.js')
|
|
8
|
+
const { assert, bail } = require('./asserts.js')
|
|
9
|
+
const { LEGACY_OPLOG_VERSION } = require('./constants.js')
|
|
10
|
+
|
|
11
|
+
const { getEncoding } = require('../encoding/spec/autobee')
|
|
12
|
+
|
|
13
|
+
const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
|
|
14
|
+
const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
|
|
15
|
+
|
|
16
|
+
const EMPTY = b4a.alloc(0)
|
|
17
|
+
const INDEX_VERSION = 1
|
|
18
|
+
const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
19
|
+
|
|
20
|
+
// indexer rotations chain legacy systems - how many generations we chase
|
|
21
|
+
const MAX_MIGRATE_HOPS = 64
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
MAX_MIGRATE_HOPS,
|
|
25
|
+
checkAutobaseMigration,
|
|
26
|
+
readLegacySystemInfo,
|
|
27
|
+
findNewestSystem,
|
|
28
|
+
resolveLegacyViews,
|
|
29
|
+
inflateLegacyOplog,
|
|
30
|
+
decodeLegacySystemInfo,
|
|
31
|
+
deriveNamespace,
|
|
32
|
+
coreLength
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// boot-time migration of local legacy autobase storage
|
|
36
|
+
async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
|
|
37
|
+
const bootRecord = await local.getUserData('autobase/boot')
|
|
38
|
+
if (!bootRecord) return null
|
|
39
|
+
|
|
40
|
+
const { key } = encoding.decodeAutobaseBootRecord(bootRecord)
|
|
41
|
+
const core = store.get({ key: key, encryption: null })
|
|
42
|
+
await core.ready()
|
|
43
|
+
|
|
44
|
+
const system = { key, length: core.length }
|
|
45
|
+
|
|
46
|
+
// setup encryption
|
|
47
|
+
const encryptionKey = await local.getUserData('autobase/encryption')
|
|
48
|
+
await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
|
|
49
|
+
|
|
50
|
+
const catchup = await getCatchupHeads(core)
|
|
51
|
+
const nodes = await Promise.all(
|
|
52
|
+
catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const info = await readLegacySystemInfo(core, core.length)
|
|
56
|
+
|
|
57
|
+
const indexerManifests = await Promise.all(
|
|
58
|
+
info.indexers.map((idx) => storeCoreManifest(store, idx.key))
|
|
59
|
+
)
|
|
60
|
+
const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
|
|
61
|
+
const entropy = legacyEntropy(info, indexerManifests)
|
|
62
|
+
|
|
63
|
+
const getManifest = (key) => storeCoreManifest(store, key)
|
|
64
|
+
|
|
65
|
+
const mappedViews = new Map()
|
|
66
|
+
for (const name of legacyViews) {
|
|
67
|
+
const v = await findViewByName(
|
|
68
|
+
getManifest,
|
|
69
|
+
bootstrap.key,
|
|
70
|
+
encryptionKey,
|
|
71
|
+
indexerManifests,
|
|
72
|
+
views,
|
|
73
|
+
entropy,
|
|
74
|
+
name
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
mappedViews.set(name, v)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await core.close()
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
encryptionKey,
|
|
84
|
+
system,
|
|
85
|
+
views: mappedViews,
|
|
86
|
+
catchup: nodes
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// decode the legacy system info recorded in the system core's block at length
|
|
91
|
+
async function readLegacySystemInfo(core, length, opts = null) {
|
|
92
|
+
// Decode hyperbee block using hyperbee2 compat
|
|
93
|
+
const node = decodeBlock(await core.get(length - 1, opts))
|
|
94
|
+
assert(node.keys.length > 0, 'bad system block')
|
|
95
|
+
|
|
96
|
+
// Decode system info from hyperbee block using autobee compat
|
|
97
|
+
return decodeLegacySystemInfo(node.keys[0].value)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
|
|
101
|
+
// the newest legacy system that indexer knows. take the majority vote. an
|
|
102
|
+
// indexer that already moved to autobee announces its live autobee state
|
|
103
|
+
// instead - keep the newest of those as a direct fast-forward candidate and
|
|
104
|
+
// bisect down to its buried legacy stamps so its vote still counts
|
|
105
|
+
async function findNewestSystem(ff, info) {
|
|
106
|
+
const tally = new Map()
|
|
107
|
+
|
|
108
|
+
let autobee = null
|
|
109
|
+
|
|
110
|
+
for (const idx of info.indexers) {
|
|
111
|
+
const core = ff.auto.openCore(idx.key)
|
|
112
|
+
ff.cores.push(core)
|
|
113
|
+
await core.ready()
|
|
114
|
+
|
|
115
|
+
const length = await coreLength(core, ff.timeout)
|
|
116
|
+
if (!length || ff.destroyed) continue
|
|
117
|
+
|
|
118
|
+
let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
|
|
119
|
+
if (oplog === null || !oplog.op.views) continue
|
|
120
|
+
|
|
121
|
+
if (oplog.op.version > LEGACY_OPLOG_VERSION) {
|
|
122
|
+
if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
|
|
123
|
+
autobee = oplog
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
|
|
127
|
+
if (!boundary || ff.destroyed) continue
|
|
128
|
+
|
|
129
|
+
oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
|
|
130
|
+
if (oplog === null || !oplog.op.views) continue
|
|
131
|
+
if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sys = oplog.op.views.system
|
|
135
|
+
if (!sys || !sys.key) continue
|
|
136
|
+
|
|
137
|
+
const hex = b4a.toString(sys.key, 'hex')
|
|
138
|
+
const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
|
|
139
|
+
|
|
140
|
+
t.votes++
|
|
141
|
+
if (sys.length > t.length) t.length = sys.length
|
|
142
|
+
tally.set(hex, t)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const majority = (info.indexers.length >> 1) + 1
|
|
146
|
+
|
|
147
|
+
let best = null
|
|
148
|
+
for (const t of tally.values()) {
|
|
149
|
+
if (t.votes >= majority && (best === null || t.length > best.length)) best = t
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { legacy: best === null ? null : best.key, autobee }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// map the legacy views recorded in the system info by name
|
|
156
|
+
async function resolveLegacyViews(ff, info) {
|
|
157
|
+
const indexerManifests = await Promise.all(
|
|
158
|
+
info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
|
|
159
|
+
)
|
|
160
|
+
if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
|
|
161
|
+
|
|
162
|
+
const entropy = legacyEntropy(info, indexerManifests)
|
|
163
|
+
|
|
164
|
+
const getManifest = (key, length) => ffCoreManifest(ff, key, length)
|
|
165
|
+
|
|
166
|
+
const views = new Map()
|
|
167
|
+
|
|
168
|
+
await Promise.all(
|
|
169
|
+
ff.auto.legacyViews.map(async (name) => {
|
|
170
|
+
const v = await findViewByName(
|
|
171
|
+
getManifest,
|
|
172
|
+
ff.auto.key,
|
|
173
|
+
ff.auto.encryptionKey,
|
|
174
|
+
indexerManifests,
|
|
175
|
+
info.views,
|
|
176
|
+
entropy,
|
|
177
|
+
name
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if (v) views.set(name, v)
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if (ff.destroyed) return null
|
|
185
|
+
|
|
186
|
+
return views
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// legacy nodes always inflate, but only indexers carry the digest/checkpoint
|
|
190
|
+
// stamps that synthesise system info - callers must check op.views
|
|
191
|
+
async function inflateLegacyOplog(buf, core, seq, opts = null) {
|
|
192
|
+
const m = encoding.decodeRawOplog(buf)
|
|
193
|
+
|
|
194
|
+
const op = {
|
|
195
|
+
version: m.version,
|
|
196
|
+
timestamp: 0,
|
|
197
|
+
links: m.node.heads,
|
|
198
|
+
batch: { start: 0, end: m.node.batch - 1 },
|
|
199
|
+
views: null,
|
|
200
|
+
optimistic: !!m.optimistic,
|
|
201
|
+
value: m.node.value
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
|
|
205
|
+
|
|
206
|
+
const fetches = []
|
|
207
|
+
|
|
208
|
+
fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
|
|
209
|
+
fetches.push(m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf)
|
|
210
|
+
|
|
211
|
+
const [digestNode, checkpointNode] = await Promise.all(fetches)
|
|
212
|
+
// a wait: false get we could not satisfy locally
|
|
213
|
+
if (digestNode === null || checkpointNode === null) return op
|
|
214
|
+
|
|
215
|
+
const { digest } = encoding.decodeRawOplog(digestNode)
|
|
216
|
+
const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
|
|
217
|
+
|
|
218
|
+
if (!checkpoint.system || !checkpoint.system.checkpoint) return op
|
|
219
|
+
|
|
220
|
+
op.views = {
|
|
221
|
+
system: {
|
|
222
|
+
key: digest.key,
|
|
223
|
+
start: 0,
|
|
224
|
+
length: checkpoint.system.checkpoint.length
|
|
225
|
+
},
|
|
226
|
+
flushes: seq
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return op
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function decodeLegacySystemInfo(buffer) {
|
|
233
|
+
const state = { start: 0, end: buffer.length, buffer }
|
|
234
|
+
const version = c.uint.decode(state)
|
|
235
|
+
|
|
236
|
+
state.start--
|
|
237
|
+
|
|
238
|
+
switch (version) {
|
|
239
|
+
case 1:
|
|
240
|
+
return SystemInfoV1.decode(state)
|
|
241
|
+
case 2:
|
|
242
|
+
return SystemInfoV2.decode(state)
|
|
243
|
+
default:
|
|
244
|
+
bail('Expected legacy system info')
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
|
|
249
|
+
const encryptionId = crypto.hash(encryptionKey || EMPTY)
|
|
250
|
+
const version = c.encode(c.uint, INDEX_VERSION)
|
|
251
|
+
|
|
252
|
+
return crypto.hash([
|
|
253
|
+
NS_SIGNER_NAMESPACE,
|
|
254
|
+
version,
|
|
255
|
+
bootstrap,
|
|
256
|
+
encryptionId,
|
|
257
|
+
entropy,
|
|
258
|
+
b4a.from(name)
|
|
259
|
+
])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function coreLength(core, timeout) {
|
|
263
|
+
if (core.length) return core.length
|
|
264
|
+
|
|
265
|
+
return new Promise((resolve) => {
|
|
266
|
+
core.on('append', () => resolve(core.length))
|
|
267
|
+
setTimeout(resolve, timeout, 0)
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// a writer's oplog is a legacy prefix followed by autobee-format nodes -
|
|
272
|
+
// bisect for the boundary. lo is a length known to be legacy (the legacy
|
|
273
|
+
// system's recorded length for this writer), hi a seq known to be autobee.
|
|
274
|
+
// returns the legacy prefix length
|
|
275
|
+
async function findLegacyBoundary(ff, core, lo, hi) {
|
|
276
|
+
while (lo < hi) {
|
|
277
|
+
const mid = (lo + hi) >>> 1
|
|
278
|
+
|
|
279
|
+
const block = await core.get(mid, {
|
|
280
|
+
timeout: ff.timeout,
|
|
281
|
+
activeRequests: ff.activeRequests
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
|
|
285
|
+
else lo = mid + 1
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lo
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function legacyEntropy(info, indexerManifests) {
|
|
292
|
+
return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function findViewByName(
|
|
296
|
+
getManifest,
|
|
297
|
+
bootstrap,
|
|
298
|
+
encryptionKey,
|
|
299
|
+
indexerManifests,
|
|
300
|
+
views,
|
|
301
|
+
entropy,
|
|
302
|
+
name
|
|
303
|
+
) {
|
|
304
|
+
if (indexerManifests.length === 0) return null
|
|
305
|
+
|
|
306
|
+
const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
|
|
307
|
+
|
|
308
|
+
for (const v of views) {
|
|
309
|
+
const manifest = await getManifest(v.key, v.length)
|
|
310
|
+
if (!manifest) continue
|
|
311
|
+
|
|
312
|
+
if (manifest.signers.length === 0) continue
|
|
313
|
+
|
|
314
|
+
const signer = manifest.signers[0]
|
|
315
|
+
|
|
316
|
+
if (b4a.equals(signer.namespace, namespace)) return v
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// request-tracked manifest read over replication
|
|
323
|
+
async function ffCoreManifest(ff, key, length) {
|
|
324
|
+
const core = ff.auto.store.get(key)
|
|
325
|
+
ff.cores.push(core)
|
|
326
|
+
|
|
327
|
+
await core.ready()
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
if (!core.manifest) {
|
|
331
|
+
await core.get(length - 1, {
|
|
332
|
+
timeout: ff.timeout,
|
|
333
|
+
activeRequests: ff.activeRequests
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return core.manifest
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// local-storage manifest read
|
|
344
|
+
async function storeCoreManifest(store, key) {
|
|
345
|
+
const core = store.get(key)
|
|
346
|
+
await core.ready()
|
|
347
|
+
|
|
348
|
+
const manifest = core.manifest
|
|
349
|
+
await core.close()
|
|
350
|
+
|
|
351
|
+
return manifest
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function getPersistedView(store, view) {
|
|
355
|
+
const core = store.get(view.key)
|
|
356
|
+
await core.ready()
|
|
357
|
+
|
|
358
|
+
const length = core.length
|
|
359
|
+
await core.close()
|
|
360
|
+
|
|
361
|
+
return { key: view.key, length }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function getCatchupHeads(core) {
|
|
365
|
+
const session = core.session({ name: 'batch' })
|
|
366
|
+
await session.ready()
|
|
367
|
+
|
|
368
|
+
const nodes = []
|
|
369
|
+
for (let i = core.length; i < session.length; i++) {
|
|
370
|
+
const node = await session.get(i, { wait: false })
|
|
371
|
+
if (!node) throw new Error('Expect nodes to exist locally')
|
|
372
|
+
|
|
373
|
+
const entry = decodeBlock(node).keys[0]
|
|
374
|
+
if (entry.key[0] !== 0x01) continue
|
|
375
|
+
|
|
376
|
+
const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
|
|
377
|
+
if (!length) continue
|
|
378
|
+
nodes.push({ key, length })
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await session.close()
|
|
382
|
+
|
|
383
|
+
return nodes
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
|
|
387
|
+
const batch = []
|
|
388
|
+
|
|
389
|
+
const core = store.get(head.key)
|
|
390
|
+
await core.ready()
|
|
391
|
+
|
|
392
|
+
if (encryptionKey) {
|
|
393
|
+
await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let seq = head.length - 1
|
|
397
|
+
const block = await core.get(seq--, { wait: false })
|
|
398
|
+
if (!block) throw new Error('Expect writer node to exist locally')
|
|
399
|
+
|
|
400
|
+
const node = encoding.decodeOplog(block)
|
|
401
|
+
|
|
402
|
+
batch.unshift({ ...head, ...node, from: core })
|
|
403
|
+
|
|
404
|
+
while (seq >= 0) {
|
|
405
|
+
const block = await core.get(seq--, { wait: false })
|
|
406
|
+
if (!block) break
|
|
407
|
+
|
|
408
|
+
const node = encoding.decodeOplog(block)
|
|
409
|
+
if (!node.batch.end) break
|
|
410
|
+
|
|
411
|
+
batch.unshift({ key: head.key, length: seq + 2, from: core, ...node })
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return batch
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// lazy to avoid the require cycle - fast-forward imports this module
|
|
418
|
+
function flushHead(auto, head, opts) {
|
|
419
|
+
const FastForward = require('./fast-forward.js')
|
|
420
|
+
return FastForward.flushHead(auto, head, opts)
|
|
421
|
+
}
|