autobee 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,8 @@
1
1
  const safetyCatch = require('safety-catch')
2
- const b4a = require('b4a')
3
2
  const Hypercore = require('hypercore')
4
3
 
5
4
  const System = require('./system.js')
6
5
  const migrations = require('./migrations.js')
7
- const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
8
6
 
9
7
  const DEFAULT_OP_TIMEOUT = 5_000
10
8
  const MIN_FF_GAP = 32
@@ -17,7 +15,7 @@ module.exports = class FastForward {
17
15
 
18
16
  // 'reboot' is a storage namespace: renaming it would derive a different
19
17
  // scratch core on disk, so it stays as is
20
- this.system = new System(auto.store.namespace('reboot'), null, {
18
+ this.system = new System(auto, auto.store.namespace('reboot'), {
21
19
  getEncryptionProvider: this.auto.getSystemEncryption,
22
20
  encrypted: this.encrypted,
23
21
  activeRequests: this.activeRequests
@@ -34,12 +32,7 @@ module.exports = class FastForward {
34
32
 
35
33
  static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
36
34
 
37
- static async fromHead(
38
- auto,
39
- head,
40
- trusted,
41
- { force = false, timeout = 0, condition = null, reference = null } = {}
42
- ) {
35
+ static async fromHead(auto, head, trusted, { force = false, timeout = 0 } = {}) {
43
36
  const conservative = !force && auto._conservativeFF
44
37
  const timeoutOpts = timeout ? { timeout } : null
45
38
 
@@ -59,19 +52,11 @@ module.exports = class FastForward {
59
52
  return null
60
53
  }
61
54
 
62
- if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
63
- return null
64
- }
65
-
66
55
  return new FastForward(auto, batchToHead(verified.op.views.system))
67
56
  }
68
57
 
69
- static async fromHeads(
70
- auto,
71
- heads,
72
- { force = false, timeout = 0, condition = null, reference = null } = {}
73
- ) {
74
- if (reference === null) reference = auto._workingView.view
58
+ static async fromHeads(auto, heads, { force = false, timeout = 0 } = {}) {
59
+ const reference = auto._workingView.view
75
60
 
76
61
  const promises = []
77
62
 
@@ -112,12 +97,7 @@ module.exports = class FastForward {
112
97
  }
113
98
 
114
99
  if (bestTrusted !== null) {
115
- return FastForward.fromHead(auto, bestTrusted, null, {
116
- force,
117
- timeout,
118
- condition,
119
- reference
120
- })
100
+ return FastForward.fromHead(auto, bestTrusted, null, { force, timeout })
121
101
  }
122
102
 
123
103
  candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
@@ -127,12 +107,7 @@ module.exports = class FastForward {
127
107
  if (trusted === null) continue
128
108
  if (auto.fastForwarding || auto.fastForwardTo) return null
129
109
 
130
- const ff = await FastForward.fromHead(auto, res, trusted, {
131
- force,
132
- timeout,
133
- condition,
134
- reference
135
- })
110
+ const ff = await FastForward.fromHead(auto, res, trusted, { force, timeout })
136
111
  if (ff !== null) return ff
137
112
  }
138
113
 
@@ -151,17 +126,6 @@ module.exports = class FastForward {
151
126
  }
152
127
  }
153
128
 
154
- static async accepts(auto, oplog, condition, reference) {
155
- const opened = auto.openViewAt(oplog)
156
- if (opened === null) return false
157
-
158
- try {
159
- return !!(await condition(opened.view, reference))
160
- } finally {
161
- await opened.close()
162
- }
163
- }
164
-
165
129
  static async mostRecentTrusted(auto, head, reference) {
166
130
  const opened = auto.openViewAt(head)
167
131
  if (opened === null) return null
@@ -198,38 +162,6 @@ module.exports = class FastForward {
198
162
 
199
163
  await this.system.boot(this.head, { timeout: this.timeout })
200
164
 
201
- // a legacy system needs migrating rather than a plain fast-forward
202
- if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
203
- const migrated = this.auto._migratedHead && this.auto._migratedHead.system
204
- if (
205
- migrated &&
206
- b4a.equals(migrated.key, this.head.key) &&
207
- migrated.length >= this.head.length
208
- ) {
209
- return null
210
- }
211
-
212
- const result = await this._migrate()
213
- if (result === null || result.autobee === null) return result
214
-
215
- // an indexer of the newest legacy generation already moved to autobee -
216
- // fast-forward straight onto its announced head instead of migrating,
217
- // falling back to the legacy migration if the head cannot be booted
218
- try {
219
- this.head = result.autobee
220
- await this.system.boot(this.head, { timeout: this.timeout })
221
- if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
222
- throw new Error('Expected an autobee system')
223
- }
224
- } catch (err) {
225
- safetyCatch(err)
226
- this.head = result.head
227
- return { head: result.head, migrate: result.migrate }
228
- }
229
-
230
- // fall through to the plain fast-forward below
231
- }
232
-
233
165
  const promises = []
234
166
 
235
167
  // ensure local key is locally available always
@@ -252,70 +184,12 @@ module.exports = class FastForward {
252
184
  await Promise.all(promises)
253
185
  if (this.destroyed) return null
254
186
 
255
- if (this.destroyed) return null
256
-
257
187
  return {
258
188
  head: this.head,
259
189
  migrate: null
260
190
  }
261
191
  }
262
192
 
263
- // Upgrade a legacy (pre-AUTOBEE_VERSION) system in place. The system already
264
- // booted above (coerced to the current struct), but the original v1/v2 struct
265
- // is needed to recover the indexers/views/entropy, so decode the raw block.
266
- //
267
- // A head may be arbitrarily old: an indexer rotation freezes the system core
268
- // and moves the indexers to a successor. The frozen core cannot reveal its
269
- // successor (the promotion only lands in its unsigned tail), but the
270
- // indexers' newest oplog digests stamp the newest system they know - chase
271
- // those so the migration lands on the newest generation instead of the head
272
- // we were given.
273
- async _migrate() {
274
- let head = { key: this.head.key, length: this.head.length }
275
- const seen = new Set()
276
-
277
- for (let hops = 0; hops < migrations.MAX_MIGRATE_HOPS; hops++) {
278
- seen.add(b4a.toString(head.key, 'hex'))
279
-
280
- const core = this.auto.store.get({ key: head.key, encryption: null })
281
- this.cores.push(core)
282
- await core.ready()
283
-
284
- // setup encryption
285
- await core.setEncryption(this.auto.getSystemEncryption())
286
-
287
- // successors are resolved by key only - wait for the replicated length
288
- if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
289
- if (!head.length || this.destroyed) return null
290
-
291
- const info = await migrations.readLegacySystemInfo(core, head.length, {
292
- timeout: this.timeout,
293
- activeRequests: this.activeRequests
294
- })
295
-
296
- const { legacy, autobee } = await migrations.findNewestSystem(this, info)
297
- if (this.destroyed) return null
298
-
299
- if (legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
300
- head = { key: legacy, length: 0 }
301
- continue
302
- }
303
-
304
- this.head = head
305
-
306
- const views = await migrations.resolveLegacyViews(this, info)
307
- if (views === null || this.destroyed) return null
308
-
309
- return {
310
- head,
311
- migrate: views,
312
- autobee: autobee === null ? null : batchToHead(autobee.op.views.system)
313
- }
314
- }
315
-
316
- return null
317
- }
318
-
319
193
  async _resolveLength() {
320
194
  const core = this.auto.store.get({ key: this.head.key })
321
195
  this.cores.push(core)
package/lib/migrations.js CHANGED
@@ -6,7 +6,6 @@ const { decodeBlock } = require('hyperbee2/lib/encoding.js')
6
6
 
7
7
  const encoding = require('./encoding.js')
8
8
  const { assert, bail } = require('./asserts.js')
9
- const { LEGACY_OPLOG_VERSION } = require('./constants.js')
10
9
 
11
10
  const { getEncoding } = require('../encoding/spec/autobee')
12
11
 
@@ -18,23 +17,52 @@ const INDEX_VERSION = 1
18
17
  const INFO = b4a.from([0x00, 0x00, 0x69, 0x6e, 0x66, 0x6f])
19
18
  const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
20
19
 
21
- // indexer rotations chain legacy systems - how many generations we chase
22
- const MAX_MIGRATE_HOPS = 64
23
-
24
20
  module.exports = {
25
- MAX_MIGRATE_HOPS,
21
+ getCoreManifest,
22
+ getViewMap,
26
23
  checkAutobaseMigration,
27
- readLegacySystemInfo,
28
- findNewestSystem,
29
- resolveLegacyViews,
30
24
  inflateLegacyOplog,
31
- decodeLegacySystemInfo,
32
- deriveNamespace,
33
25
  coreLength
34
26
  }
35
27
 
28
+ async function getCoreManifest(store, key, { timeout, wait = true } = {}) {
29
+ const core = store.get(key)
30
+ await core.ready()
31
+ try {
32
+ if (!core.manifest && core.length === 0) await core.get(0, { timeout, wait })
33
+ return core.manifest
34
+ } finally {
35
+ await core.close()
36
+ }
37
+ }
38
+
39
+ // local-only probes: the manifests are expected in storage already (prefetched
40
+ // by the reset path), so a violation skips instead of hanging on the network
41
+ async function getViewMap(store, bootstrap, encryptionKey, info, legacyViews) {
42
+ const getIndexerManifest = ({ key }) => getCoreManifest(store, key, { wait: false })
43
+ const indexerManifests = await Promise.all(info.indexers.map(getIndexerManifest))
44
+ const entropy = legacyEntropy(info, indexerManifests)
45
+ const mappedViews = new Map()
46
+
47
+ for (const name of legacyViews) {
48
+ const v = await findViewByName(
49
+ store,
50
+ bootstrap.key,
51
+ encryptionKey,
52
+ indexerManifests,
53
+ info.views,
54
+ entropy,
55
+ name
56
+ )
57
+
58
+ mappedViews.set(name, v)
59
+ }
60
+
61
+ return mappedViews
62
+ }
63
+
36
64
  // boot-time migration of local legacy autobase storage
37
- async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
65
+ async function checkAutobaseMigration(store, local, bootstrap) {
38
66
  const bootRecord = await local.getUserData('autobase/boot')
39
67
  if (!bootRecord) return null
40
68
 
@@ -60,41 +88,19 @@ async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
60
88
  await session.close()
61
89
  }
62
90
 
63
- const info = await readLegacySystemInfo(core, length, { wait: false })
91
+ // nothing reads the info here anymore, but a legacy system that will not
92
+ // decode has to throw for migrateWithFallback pathway
93
+ await readLegacySystemInfo(core, length, { wait: false })
64
94
 
65
95
  const nodes = await Promise.all(
66
96
  catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
67
97
  )
68
98
 
69
- const indexerManifests = await Promise.all(
70
- info.indexers.map((idx) => storeCoreManifest(store, idx.key))
71
- )
72
- const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
73
- const entropy = legacyEntropy(info, indexerManifests)
74
-
75
- const getManifest = (key) => storeCoreManifest(store, key)
76
-
77
- const mappedViews = new Map()
78
- for (const name of legacyViews) {
79
- const v = await findViewByName(
80
- getManifest,
81
- bootstrap.key,
82
- encryptionKey,
83
- indexerManifests,
84
- views,
85
- entropy,
86
- name
87
- )
88
-
89
- mappedViews.set(name, v)
90
- }
91
-
92
99
  await core.close()
93
100
 
94
101
  return {
95
102
  encryptionKey,
96
103
  system,
97
- views: mappedViews,
98
104
  catchup: nodes
99
105
  }
100
106
  }
@@ -112,95 +118,6 @@ async function readLegacySystemInfo(core, length, opts = null) {
112
118
  return decodeLegacySystemInfo(node.keys[0].value)
113
119
  }
114
120
 
115
- // read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
116
- // the newest legacy system that indexer knows. take the majority vote. an
117
- // indexer that already moved to autobee announces its live autobee state
118
- // instead - keep the newest of those as a direct fast-forward candidate and
119
- // bisect down to its buried legacy stamps so its vote still counts
120
- async function findNewestSystem(ff, info) {
121
- const tally = new Map()
122
-
123
- let autobee = null
124
-
125
- for (const idx of info.indexers) {
126
- const core = ff.auto.openCore(idx.key)
127
- ff.cores.push(core)
128
- await core.ready()
129
-
130
- const length = await coreLength(core, ff.timeout)
131
- if (!length || ff.destroyed) continue
132
-
133
- let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
134
- if (oplog === null || !oplog.op.views) continue
135
-
136
- if (oplog.op.version > LEGACY_OPLOG_VERSION) {
137
- if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
138
- autobee = oplog
139
- }
140
-
141
- const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
142
- if (!boundary || ff.destroyed) continue
143
-
144
- oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
145
- if (oplog === null || !oplog.op.views) continue
146
- if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
147
- }
148
-
149
- const sys = oplog.op.views.system
150
- if (!sys || !sys.key) continue
151
-
152
- const hex = b4a.toString(sys.key, 'hex')
153
- const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
154
-
155
- t.votes++
156
- if (sys.length > t.length) t.length = sys.length
157
- tally.set(hex, t)
158
- }
159
-
160
- const majority = (info.indexers.length >> 1) + 1
161
-
162
- let best = null
163
- for (const t of tally.values()) {
164
- if (t.votes >= majority && (best === null || t.length > best.length)) best = t
165
- }
166
-
167
- return { legacy: best === null ? null : best.key, autobee }
168
- }
169
-
170
- // map the legacy views recorded in the system info by name
171
- async function resolveLegacyViews(ff, info) {
172
- const indexerManifests = await Promise.all(
173
- info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
174
- )
175
- if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
176
-
177
- const entropy = legacyEntropy(info, indexerManifests)
178
-
179
- const getManifest = (key, length) => ffCoreManifest(ff, key, length)
180
-
181
- const views = new Map()
182
-
183
- await Promise.all(
184
- ff.auto.legacyViews.map(async (name) => {
185
- const v = await findViewByName(
186
- getManifest,
187
- ff.auto.key,
188
- ff.auto.encryptionKey,
189
- indexerManifests,
190
- info.views,
191
- entropy,
192
- name
193
- )
194
-
195
- if (v) views.set(name, v)
196
- })
197
- )
198
-
199
- if (ff.destroyed) return null
200
-
201
- return views
202
- }
203
-
204
121
  // legacy nodes always inflate, but only indexers carry the digest/checkpoint
205
122
  // stamps that synthesise system info - callers must check op.views
206
123
  async function inflateLegacyOplog(buf, core, seq, timeout) {
@@ -309,32 +226,12 @@ function coreLength(core, timeout) {
309
226
  })
310
227
  }
311
228
 
312
- // a writer's oplog is a legacy prefix followed by autobee-format nodes -
313
- // bisect for the boundary. lo is a length known to be legacy (the legacy
314
- // system's recorded length for this writer), hi a seq known to be autobee.
315
- // returns the legacy prefix length
316
- async function findLegacyBoundary(ff, core, lo, hi) {
317
- while (lo < hi) {
318
- const mid = (lo + hi) >>> 1
319
-
320
- const block = await core.get(mid, {
321
- timeout: ff.timeout,
322
- activeRequests: ff.activeRequests
323
- })
324
-
325
- if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
326
- else lo = mid + 1
327
- }
328
-
329
- return lo
330
- }
331
-
332
229
  function legacyEntropy(info, indexerManifests) {
333
230
  return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
334
231
  }
335
232
 
336
233
  async function findViewByName(
337
- getManifest,
234
+ store,
338
235
  bootstrap,
339
236
  encryptionKey,
340
237
  indexerManifests,
@@ -347,7 +244,7 @@ async function findViewByName(
347
244
  const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
348
245
 
349
246
  for (const v of views) {
350
- const manifest = await getManifest(v.key, v.length)
247
+ const manifest = await getCoreManifest(store, v.key, { wait: false })
351
248
  if (!manifest) continue
352
249
 
353
250
  if (manifest.signers.length === 0) continue
@@ -360,50 +257,6 @@ async function findViewByName(
360
257
  return null
361
258
  }
362
259
 
363
- // request-tracked manifest read over replication
364
- async function ffCoreManifest(ff, key, length) {
365
- const core = ff.auto.store.get(key)
366
- ff.cores.push(core)
367
-
368
- await core.ready()
369
-
370
- try {
371
- if (!core.manifest) {
372
- await core.get(length - 1, {
373
- timeout: ff.timeout,
374
- activeRequests: ff.activeRequests
375
- })
376
- }
377
- } catch {
378
- return null
379
- }
380
-
381
- return core.manifest
382
- }
383
-
384
- // local-storage manifest read
385
- async function storeCoreManifest(store, key) {
386
- const core = store.get(key)
387
- await core.ready()
388
-
389
- const manifest = core.manifest
390
- await core.close()
391
-
392
- return manifest
393
- }
394
-
395
- async function getPersistedView(store, view) {
396
- const core = store.get(view.key)
397
- await core.ready()
398
-
399
- // core.length can run past what was downloaded - the system's recorded
400
- // length is the last one we know is local
401
- const length = Math.min(core.length, view.length)
402
- await core.close()
403
-
404
- return { key: view.key, length }
405
- }
406
-
407
260
  function getOplog(info, batch) {
408
261
  if (batch.length === 1) return batch[0].keys[0]
409
262
 
@@ -489,9 +342,3 @@ async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
489
342
 
490
343
  return batch
491
344
  }
492
-
493
- // lazy to avoid the require cycle - fast-forward imports this module
494
- function flushHead(auto, head, opts) {
495
- const FastForward = require('./fast-forward.js')
496
- return FastForward.flushHead(auto, head, opts)
497
- }
package/lib/system.js CHANGED
@@ -2,7 +2,9 @@ const Hyperbee = require('hyperbee2')
2
2
  const Promotions = require('./promotions.js')
3
3
  const b4a = require('b4a')
4
4
 
5
- const { AUTOBEE_VERSION } = require('./constants')
5
+ const { getViewMap, getCoreManifest } = require('./migrations.js')
6
+
7
+ const { AUTOBEE_VERSION, LEGACY_AUTOBASE_VERSION } = require('./constants')
6
8
  const encoding = require('./encoding.js')
7
9
  const topo = require('./topo.js')
8
10
 
@@ -33,12 +35,13 @@ class SystemSnapshot {
33
35
  }
34
36
 
35
37
  module.exports = class Systembee {
36
- constructor(store, name, opts = {}) {
37
- this.name = name // for debuggin
38
+ constructor(auto, store, opts = {}) {
39
+ this.auto = auto
38
40
  this.store = store
39
41
  this.bee = new Hyperbee(store, opts)
40
42
  this.view = null
41
43
  this.heads = []
44
+ this.migration = null
42
45
  this.version = 0
43
46
  this.timestamp = 0
44
47
  this.flushes = 0
@@ -48,6 +51,10 @@ module.exports = class Systembee {
48
51
  this.promotions = new Promotions(this.bee)
49
52
  }
50
53
 
54
+ get name() {
55
+ return this.auto.name
56
+ }
57
+
51
58
  async addWriter(
52
59
  key,
53
60
  { length = 0, weight = 1, isGenesis = false, coord = null, carrier = -1, anchor = false } = {}
@@ -127,9 +134,17 @@ module.exports = class Systembee {
127
134
 
128
135
  this.version = info ? info.version : 0
129
136
  this.view = info ? info.view : EMPTY_HEAD
137
+ this.migration = null
130
138
  this.heads = info ? info.heads : []
131
139
  this.timestamp = info ? info.timestamp : 0
132
140
  this.flushes = info ? info.flushes : 0
141
+
142
+ if (this.version <= LEGACY_AUTOBASE_VERSION) {
143
+ // ~~~ good enough estimation
144
+ this.flushes = Math.floor(this.bee.core.length / 2)
145
+ this.view = await this._migrate(info, { timeout })
146
+ }
147
+
133
148
  this.updates.clear()
134
149
  this.writers.clear()
135
150
  this.promotions.reset(info)
@@ -139,6 +154,29 @@ module.exports = class Systembee {
139
154
  }
140
155
  }
141
156
 
157
+ async _migrate(info, { timeout } = {}) {
158
+ if (!info) return EMPTY_HEAD
159
+ const a = this.auto
160
+
161
+ const toManifests = ({ key }) => getCoreManifest(a.store, key, { timeout })
162
+ const promises = [...info.indexers.map(toManifests), ...info.views.map(toManifests)]
163
+
164
+ await Promise.all(promises)
165
+
166
+ const views = await getViewMap(a.store, a.bootstrap, a.encryptionKey, info, a.legacyViews)
167
+
168
+ // the handler only runs once the head is locked in - see auto._runMigration
169
+ this.migration = { views, head: this.bee.head() }
170
+
171
+ // legacyViews is ordered by preference, so the first one that resolved wins
172
+ for (const name of a.legacyViews) {
173
+ const view = views.get(name)
174
+ if (view) return view
175
+ }
176
+
177
+ return EMPTY_HEAD
178
+ }
179
+
142
180
  bootRecord() {
143
181
  const system = this.bee.head()
144
182
  return system.length ? system : null
@@ -441,21 +479,3 @@ module.exports = class Systembee {
441
479
  return this.promotions.hint(key, opts)
442
480
  }
443
481
  }
444
-
445
- function getInfo(data) {
446
- for (const { keys } of data.batch) {
447
- for (const k of keys) {
448
- const prefix = k.key[0]
449
-
450
- if (prefix === 0) {
451
- try {
452
- return encoding.decodeSystemInfo(k.value)
453
- } catch {
454
- return null
455
- }
456
- }
457
- }
458
- }
459
-
460
- return null
461
- }
package/lib/topo.js CHANGED
@@ -7,15 +7,11 @@ const { LEGACY_OPLOG_VERSION } = require('./constants.js')
7
7
  const EMPTY_HEAD = { length: 0, key: null }
8
8
  const INFO_LEGACY_KEY = b4a.concat([b4a.from([0, 0]), b4a.from('info')])
9
9
 
10
- const DEFAULT_OP_TIMEOUT = 5000
11
-
12
10
  exports.sort = sort
13
11
  exports.replay = replay
14
- exports.getMostRecentHead = getMostRecentHead
15
12
 
16
13
  exports.isLinking = isLinking
17
14
  exports.isLinkingAll = isLinkingAll
18
- exports.isLegacyNode = isLegacyNode
19
15
 
20
16
  class Clock {
21
17
  constructor() {
@@ -35,19 +31,6 @@ class Clock {
35
31
  }
36
32
  }
37
33
 
38
- async function getMostRecentHead(auto, system) {
39
- const ch = system.createChangesStream()
40
- for await (const data of ch) {
41
- const refs = getOplogCompat(data)
42
- if (!refs.length) continue
43
-
44
- const { oplog } = refs[0]
45
- return { key: oplog.key, length: oplog.length }
46
- }
47
-
48
- return null
49
- }
50
-
51
34
  // nodes we cannot resolve are returned as null rather than dropped, so callers
52
35
  // can tell an incomplete replay from a short one
53
36
  async function replay(auto) {
@@ -181,7 +164,7 @@ function cmp(a, b) {
181
164
  return a.length < b.length ? -1 : 1
182
165
  }
183
166
 
184
- async function inflate(auto, reorder, { wait = true, timeout = DEFAULT_OP_TIMEOUT } = {}) {
167
+ async function inflate(auto, reorder, { wait = true, timeout = 0 } = {}) {
185
168
  const readOpts = wait ? { timeout } : { wait: false }
186
169
 
187
170
  const promises = []
@@ -233,7 +216,7 @@ async function getOplogBatch(undo, core, length, weight, index, readOpts = null)
233
216
  const legacy = oplog.version <= LEGACY_OPLOG_VERSION
234
217
 
235
218
  // if this is legacy the start pointer is always 0, fix
236
- if (legacy) {
219
+ if (legacy && !oplog.links.length) {
237
220
  for (let i = length - 1; i > 0; i--) {
238
221
  const block = await core.get(i - 1, readOpts)
239
222
  if (block === null) return null // wait: false in replay helper
@@ -241,6 +224,7 @@ async function getOplogBatch(undo, core, length, weight, index, readOpts = null)
241
224
  const oplog = encoding.decodeOplog(block)
242
225
  if (oplog.batch.end === 0) break
243
226
  head.batch.start++
227
+ if (oplog.links.length > 0) break // always start of batch in legacy
244
228
  }
245
229
  }
246
230
 
@@ -345,6 +329,8 @@ function getOplogCompat(data) {
345
329
  const candidates = []
346
330
 
347
331
  for (const { keys } of data.batch) {
332
+ if (!keys) continue
333
+
348
334
  for (const k of keys) {
349
335
  if (k.key[0] !== 1) continue
350
336