autobee 2.0.1 → 2.1.1

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 CHANGED
@@ -57,7 +57,7 @@ module.exports = class Autobee extends ReadyResource {
57
57
  // defer one tick to ensure consistent state, then return state prom
58
58
  preload: async () => {
59
59
  await 1
60
- await this._bootReady()
60
+ await this._bootGuard.ready()
61
61
  },
62
62
  getEncryptionProvider: this.getViewEncryption
63
63
  })
@@ -123,9 +123,11 @@ module.exports = class Autobee extends ReadyResource {
123
123
 
124
124
  this.legacyViews = handlers.legacyViews || []
125
125
 
126
+ // the guards may be destroyed before anyone waits on them
126
127
  this._bootGuard = new ReadyGuard()
127
- this._bootingState = null
128
- this._bootingAll = null
128
+ this._bootGuard.ready().catch(safetyCatch)
129
+ this._bootOnlineGuard = new ReadyGuard()
130
+ this._bootOnlineGuard.ready().catch(safetyCatch)
129
131
 
130
132
  this._now = handlers.now || Date.now // overridable for clock-drift tests
131
133
  this._preapply = handlers.preapply || null
@@ -135,7 +137,6 @@ module.exports = class Autobee extends ReadyResource {
135
137
  this._needsUpdate = false
136
138
  this._approvalCheck = true
137
139
  this._approvedPending = new Map()
138
- this._approvalRequests = []
139
140
  this._prefetchingApprovals = false
140
141
  this._updateLocalCore = null
141
142
  this._host = new ApplyCalls(this)
@@ -195,26 +196,46 @@ module.exports = class Autobee extends ReadyResource {
195
196
  }
196
197
  }
197
198
 
199
+ // network free: only a migration needs peers, so ready() awaits the full
200
+ // state boot unless one is pending and defers it to the background if so -
201
+ // anything needing the booted system must wait on the boot guard
198
202
  async _open() {
199
203
  this._prebooting = this._preBoot()
200
204
 
201
- this._bootingState = this._bootState()
202
- this._bootingAll = this._bootAll()
205
+ let result = null
203
206
 
204
- this._bootingState.catch(safetyCatch)
205
- this._bootingAll.catch(safetyCatch)
207
+ try {
208
+ result = await this._prebooting
209
+
210
+ this.key = result.key
211
+ this.bootstrap = result.bootstrap
212
+ this.discoveryKey = result.bootstrap.core.discoveryKey
213
+ this.id = result.bootstrap.core.id
214
+ this.encryptionKey = result.encryptionKey
215
+ this.previousDrain = result.previousDrain
216
+ this.local = result.local
217
+
218
+ if (this.encrypted && this.encryptionKey === null) {
219
+ throw new Error('Encryption key is expected')
220
+ }
206
221
 
207
- await this._prebooting
222
+ this.local.setEncryption(this._getEncryptionProvider())
223
+ this.writers = new ActiveWriters(this)
224
+ } catch (err) {
225
+ // unblock the guard awaiters (bee preloads, bumps, wakeups, flushes)
226
+ this._bootGuard.destroy(err)
227
+ this._bootOnlineGuard.destroy(err)
228
+ throw err
229
+ }
208
230
 
209
- await this.bee.ready()
210
- await this._workingBee.ready()
211
- await this._bootingState
231
+ const booting = this._boot()
212
232
 
213
- this._localSystemStart = this.system.bee.context.local.length
214
- this._localViewStart = this._workingBee.context.local.length
215
- this._localFlushes = this.system.flushes
233
+ // waits on the boot guard internally and destroys its own guard on failure
234
+ this._bootOnline()
216
235
 
217
- this.bumpSoon()
236
+ // if migrating, we might need network io, if not lets wait on it - less surprises
237
+ if (result.migration) booting.catch(this._onErrorBound)
238
+ else await booting
218
239
  }
219
240
 
220
241
  _requestWakeup() {
@@ -266,43 +287,40 @@ module.exports = class Autobee extends ReadyResource {
266
287
 
267
288
  async _close() {
268
289
  this._interrupting = true
269
- Hypercore.destroyRequests(this._approvalRequests, null)
270
- if (this._bootWait !== null) this._bootWait.resolve()
271
- if (this._notifyHandler) this._notifyHandler.destroy()
272
- if (this._draining) {
273
- // drain may be waiting on replicated blocks
274
- this._clearRequests()
275
- await this._draining
276
- }
277
290
 
278
- if (this._updating) await this._updating
279
-
280
- // let in-flight writer adds finish
281
291
  try {
282
- await this._bootingAll
283
- } catch {}
284
-
285
- await ApplyView.close(this.view, this)
292
+ await ApplyView.close(this.view, this)
293
+ } catch (err) {
294
+ safetyCatch(err)
295
+ }
286
296
 
287
297
  if (this.writers) await this.writers.close()
288
298
  await this.local.close()
289
299
  await this.system.close()
290
300
  await this._wakeup.close()
291
- if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
292
- await this._workingView.close()
293
- await this.bee.close()
294
- await this.store.close()
295
- }
301
+ if (this.bootstrap) await this.bootstrap.close()
296
302
 
297
- _clearRequests() {
298
- const closingError = new Error('Autobee is closing')
299
- for (const session of this.store.sessions) {
300
- if (session.opened && !session.closing) {
301
- session.clearRequests(session.activeRequests, closingError)
302
- }
303
+ try {
304
+ await this._workingView.close()
305
+ await this.bee.close()
306
+ } catch (err) {
307
+ safetyCatch(err)
303
308
  }
304
309
 
305
- if (this.fastForwarding) this.fastForwarding.clearRequests(closingError)
310
+ // don't rely on the store teardown to stop the notify watcher
311
+ if (this._notifyHandler) this._notifyHandler.destroy()
312
+
313
+ // rugpull the rest
314
+ await this.store.close()
315
+
316
+ if (this._bootWait !== null) this._bootWait.resolve()
317
+ if (this._updating) await this._updating
318
+ if (this._draining) await this._draining
319
+
320
+ // let in-flight writer adds finish
321
+ try {
322
+ await this._bootOnlineGuard.ready()
323
+ } catch {}
306
324
  }
307
325
 
308
326
  replicate(...args) {
@@ -312,7 +330,7 @@ module.exports = class Autobee extends ReadyResource {
312
330
  }
313
331
 
314
332
  async flush() {
315
- await this._bootingAll
333
+ await this._bootOnlineGuard.ready()
316
334
  }
317
335
 
318
336
  hintWakeup(wakeup) {
@@ -338,6 +356,10 @@ module.exports = class Autobee extends ReadyResource {
338
356
  return new WriterEncryption(this)
339
357
  }
340
358
 
359
+ getMostRecentHead() {
360
+ return topo.getMostRecentHead(this, this.system.bee.snapshot())
361
+ }
362
+
341
363
  async _preBoot() {
342
364
  if (this._handlers.wait) await this._handlers.wait()
343
365
 
@@ -361,15 +383,11 @@ module.exports = class Autobee extends ReadyResource {
361
383
  })
362
384
  }
363
385
 
364
- getMostRecentHead() {
365
- return topo.getMostRecentHead(this, this.system.bee.snapshot())
366
- }
367
-
368
- async _bootState() {
386
+ async _boot() {
369
387
  if (!this._bootGuard.enter()) return this._bootGuard.ready()
370
388
 
371
389
  try {
372
- await this._bootStateUnsafe()
390
+ await this._bootUnsafe()
373
391
  } catch (err) {
374
392
  this._bootGuard.destroy(err)
375
393
  throw err
@@ -377,38 +395,14 @@ module.exports = class Autobee extends ReadyResource {
377
395
 
378
396
  this._bootGuard.exit()
379
397
 
380
- return this._bootGuard.ready()
381
- }
398
+ this.bumpSoon()
382
399
 
383
- async _bootReady() {
384
- if (this._bootGuard.opened) return true
385
- try {
386
- await this._bootGuard.ready()
387
- return true
388
- } catch {
389
- return false
390
- }
400
+ return this._bootGuard.ready()
391
401
  }
392
402
 
393
- async _bootStateUnsafe() {
403
+ async _bootUnsafe() {
394
404
  const result = await this._prebooting
395
405
 
396
- this.key = result.key
397
- this.bootstrap = result.bootstrap
398
- this.discoveryKey = result.bootstrap.core.discoveryKey
399
- this.id = result.bootstrap.core.id
400
- this.encryptionKey = result.encryptionKey
401
- this.previousDrain = result.previousDrain
402
- this.local = result.local
403
-
404
- if (this.encrypted && this.encryptionKey === null) {
405
- throw new Error('Encryption key is expected')
406
- }
407
-
408
- this.local.setEncryption(this._getEncryptionProvider())
409
-
410
- this.writers = new ActiveWriters(this)
411
-
412
406
  if (this._handlers.wakeupCapability) {
413
407
  this.wakeupCapability = await this._handlers.wakeupCapability
414
408
  } else {
@@ -476,33 +470,53 @@ module.exports = class Autobee extends ReadyResource {
476
470
  this.bee.move(view)
477
471
 
478
472
  await this.writers.updateLocalState()
473
+
474
+ // roots are moved, so these resolve without re-entering the boot guard
475
+ await this.bee.ready()
476
+ await this._workingBee.ready()
477
+
478
+ // baseline before the guard opens so an early drain can't flush against stale offsets
479
+ this._localSystemStart = this.system.bee.context.local.length
480
+ this._localViewStart = this._workingBee.context.local.length
481
+ this._localFlushes = this.system.flushes
479
482
  }
480
483
 
481
- async _bootAll() {
482
- if (!(await this._bootReady())) return
484
+ async _bootOnline() {
485
+ if (!this._bootOnlineGuard.enter()) return
483
486
 
484
- await this._prebooting
487
+ try {
488
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
485
489
 
486
- for (const head of this.system.heads) {
487
- await this.writers.add(head.key)
488
- }
490
+ for (const head of this.system.heads) {
491
+ await this.writers.add(head.key)
492
+ }
489
493
 
490
- if (!this.system.heads.length) {
491
- await this.writers.add(this.bootstrap.key)
494
+ if (!this.system.heads.length) {
495
+ await this.writers.add(this.bootstrap.key)
496
+ }
497
+
498
+ await this._bump(true)
499
+ } catch (err) {
500
+ this._bootOnlineGuard.destroy(err)
501
+ return
492
502
  }
493
503
 
494
- await this._bump()
504
+ this._bootOnlineGuard.exit()
495
505
  }
496
506
 
497
507
  bumpSoon() {
498
- this._bump().catch(safetyCatch)
508
+ this._bump(false).catch(safetyCatch)
499
509
  }
500
510
 
501
- async _bump() {
502
- if (!(await this._bootReady())) return
511
+ async _bump(force) {
512
+ if (!force && !this._bootGuard.opened) await this._bootGuard.ready()
503
513
 
514
+ // resolve before the bootAll gate: a parked boot-from retries on any bump,
515
+ // and its drain is the one _bootOnline itself is waiting on
504
516
  if (this._bootWait !== null) this._bootWait.resolve()
505
517
 
518
+ if (!force && !this._bootOnlineGuard.opened) await this._bootOnlineGuard.ready()
519
+
506
520
  this.bumping++
507
521
 
508
522
  if (!this._draining) {
@@ -513,11 +527,11 @@ module.exports = class Autobee extends ReadyResource {
513
527
  }
514
528
 
515
529
  update() {
516
- return this._bump()
530
+ return this._bump(false)
517
531
  }
518
532
 
519
533
  async updated() {
520
- if (this.opened === false) await this.ready()
534
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
521
535
  if (this._draining) return this._draining
522
536
  return Promise.resolve()
523
537
  }
@@ -816,7 +830,7 @@ module.exports = class Autobee extends ReadyResource {
816
830
  }
817
831
 
818
832
  async setLocal(key, { keyPair } = {}) {
819
- if (!this.opened) await this.ready()
833
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
820
834
  if (this.closing) throw new Error('Autobee closed')
821
835
 
822
836
  const manifest = keyPair
@@ -945,15 +959,13 @@ module.exports = class Autobee extends ReadyResource {
945
959
  if (this._prefetchingApprovals) return
946
960
  this._prefetchingApprovals = true
947
961
 
948
- const activeRequests = this._approvalRequests
949
-
950
962
  try {
951
963
  const prefetch = []
952
- const standing = await this._standing(activeRequests)
964
+ const standing = await this._standing()
953
965
  if (standing <= 0 || !this._pendingWork(standing)) return
954
- for await (const p of this._servableRequests(standing, activeRequests)) {
955
- prefetch.push(this.system.get(p.key, { activeRequests }).catch(safetyCatch))
956
- prefetch.push(this.system.grantHint(p.key, { activeRequests }).catch(safetyCatch))
966
+ for await (const p of this._servableRequests(standing)) {
967
+ prefetch.push(this.system.get(p.key).catch(safetyCatch))
968
+ prefetch.push(this.system.grantHint(p.key).catch(safetyCatch))
957
969
  }
958
970
  await Promise.allSettled(prefetch)
959
971
  } finally {
@@ -961,8 +973,8 @@ module.exports = class Autobee extends ReadyResource {
961
973
  }
962
974
  }
963
975
 
964
- async _standing(activeRequests) {
965
- const rec = await this.system.get(this.local.key, { activeRequests })
976
+ async _standing() {
977
+ const rec = await this.system.get(this.local.key)
966
978
  return currentWeight(rec)
967
979
  }
968
980
 
@@ -980,8 +992,8 @@ module.exports = class Autobee extends ReadyResource {
980
992
 
981
993
  // the requests we could serve and by how much - shared by the prefetch and
982
994
  // the collector so the two cannot drift on what counts as ours
983
- async *_servableRequests(standing, activeRequests) {
984
- for await (const p of this.system.listPendingPromotions({ activeRequests })) {
995
+ async *_servableRequests(standing) {
996
+ for await (const p of this.system.listPendingPromotions()) {
985
997
  const amount = Math.min(standing, p.weight)
986
998
  if (amount <= 0) continue
987
999
  yield { key: p.key, amount }
@@ -992,8 +1004,7 @@ module.exports = class Autobee extends ReadyResource {
992
1004
  if (!this.system.promotions.changed && !this._approvalCheck) return null
993
1005
  if (!this.writers.writable) return null
994
1006
 
995
- const activeRequests = this._approvalRequests
996
- const standing = await this._standing(activeRequests)
1007
+ const standing = await this._standing()
997
1008
  if (standing <= 0) return null
998
1009
 
999
1010
  this.system.promotions.changed = false
@@ -1004,7 +1015,7 @@ module.exports = class Autobee extends ReadyResource {
1004
1015
  const approvals = []
1005
1016
  const approving = []
1006
1017
 
1007
- for await (const p of this._servableRequests(standing, activeRequests)) {
1018
+ for await (const p of this._servableRequests(standing)) {
1008
1019
  approving.push(fetchApproval.call(this, p.key, p.amount, approvals))
1009
1020
  }
1010
1021
 
@@ -1028,10 +1039,7 @@ module.exports = class Autobee extends ReadyResource {
1028
1039
  async function fetchApproval(key, weight, approvals) {
1029
1040
  const hex = b4a.toString(key, 'hex')
1030
1041
  if ((this._approvedPending.get(hex) || 0) >= weight) return
1031
- const [target, hint] = await Promise.all([
1032
- this.system.get(key, { activeRequests }),
1033
- this.system.grantHint(key, { activeRequests })
1034
- ])
1042
+ const [target, hint] = await Promise.all([this.system.get(key), this.system.grantHint(key)])
1035
1043
  if (!target || target.isRemoved) return
1036
1044
  if (hint && hint.weight >= weight) return
1037
1045
  approvals.push({ key, weight })
@@ -1229,10 +1237,12 @@ module.exports = class Autobee extends ReadyResource {
1229
1237
  return encoding.encodeValue(value, opts)
1230
1238
  }
1231
1239
 
1240
+ // gates on the state boot only, so hints can still feed a parked boot-from -
1241
+ // the trailing bump waits for bootAll like any other drain
1232
1242
  async wakeup({ key, length }) {
1233
- if (!(await this._bootReady())) return
1243
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
1234
1244
  await this.writers.wakeup(key, length)
1235
- await this._bump()
1245
+ await this._bump(false)
1236
1246
  }
1237
1247
 
1238
1248
  async append(values, { optimistic = false } = {}) {
@@ -1242,7 +1252,7 @@ module.exports = class Autobee extends ReadyResource {
1242
1252
 
1243
1253
  if (!Array.isArray(values)) values = [values]
1244
1254
 
1245
- if (!this.opened) await this.ready()
1255
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
1246
1256
 
1247
1257
  if (!optimistic && this.writers.localWriter.isRemoved) {
1248
1258
  throw new Error('Not writable')
@@ -1283,7 +1293,7 @@ module.exports = class Autobee extends ReadyResource {
1283
1293
  batch.push(node)
1284
1294
  }
1285
1295
 
1286
- return this._bump()
1296
+ return this._bump(false)
1287
1297
  }
1288
1298
 
1289
1299
  async _flushLocal() {
@@ -1314,7 +1324,7 @@ module.exports = class Autobee extends ReadyResource {
1314
1324
  }
1315
1325
 
1316
1326
  async moveTo(head) {
1317
- if (!this.opened) await this.ready()
1327
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
1318
1328
  if (this.closing) throw new Error('Autobee closed')
1319
1329
 
1320
1330
  const ff = await FastForward.fromHead(this, head, null, { force: true })
package/lib/migrations.js CHANGED
@@ -15,6 +15,7 @@ const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
15
15
 
16
16
  const EMPTY = b4a.alloc(0)
17
17
  const INDEX_VERSION = 1
18
+ const INFO = b4a.from([0x00, 0x00, 0x69, 0x6e, 0x66, 0x6f])
18
19
  const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
19
20
 
20
21
  // indexer rotations chain legacy systems - how many generations we chase
@@ -41,15 +42,13 @@ async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
41
42
  const core = store.get({ key: key, encryption: null })
42
43
  await core.ready()
43
44
 
44
- const length = await findLocalSystemLength(core, systemLength)
45
- assert(length > 0, 'Expected system block to exist locally')
46
-
47
- const system = { key, length }
48
-
49
45
  // setup encryption
50
46
  const encryptionKey = await local.getUserData('autobase/encryption')
51
47
  await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
52
48
 
49
+ const length = await findLocalSystemLength(core, systemLength)
50
+ const system = { key, length }
51
+
53
52
  const session = core.session({ name: 'batch' })
54
53
  await session.ready()
55
54
 
@@ -284,8 +283,21 @@ function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
284
283
  // scan back for the newest block the signed core actually has locally
285
284
  async function findLocalSystemLength(core, systemLength) {
286
285
  let length = Math.min(core.length, systemLength)
287
- while (length > 0 && !(await core.has(length - 1))) length--
288
- return length
286
+
287
+ while (length > 0) {
288
+ const seq = length - 1
289
+ const node = await core.get(seq, { wait: false })
290
+
291
+ if (node) {
292
+ const blk = decodeBlock(node)
293
+ const entry = blk.keys[0]
294
+ if (b4a.equals(entry.key, INFO)) return length
295
+ }
296
+
297
+ length--
298
+ }
299
+
300
+ assert(false, 'Expected system block to exist locally')
289
301
  }
290
302
 
291
303
  function coreLength(core, timeout) {
@@ -392,28 +404,56 @@ async function getPersistedView(store, view) {
392
404
  return { key: view.key, length }
393
405
  }
394
406
 
407
+ function getOplog(info, batch) {
408
+ if (batch.length === 1) return batch[0].keys[0]
409
+
410
+ const { heads } = encoding.decodeSystemInfo(info.value)
411
+
412
+ for (const b of batch) {
413
+ const entry = b.keys[0]
414
+ const w = encoding.decodeSystemWriter(entry.key, entry.value)
415
+
416
+ for (const { key } of heads) {
417
+ if (b4a.equals(w.key, key)) return b.keys[0]
418
+ }
419
+ }
420
+
421
+ throw new Error('Could not infer oplog node')
422
+ }
423
+
395
424
  async function getCatchupHeads(session, from) {
396
425
  const seen = new Map()
397
426
  const nodes = []
398
427
 
428
+ let batch = []
429
+
399
430
  // block 0 is the bee header, never a node
400
431
  for (let i = Math.max(from, 1); i < session.length; i++) {
401
432
  const node = await session.get(i, { wait: false })
402
433
  if (!node) throw new Error('Expect nodes to exist locally')
403
434
 
404
- const entry = decodeBlock(node).keys[0]
405
- if (entry.key[0] !== 0x01) continue
435
+ const blk = decodeBlock(node)
436
+ const entry = blk.keys[0]
437
+
438
+ if (b4a.equals(entry.key, INFO)) {
439
+ const oplog = getOplog(entry, batch)
440
+ const { key, length } = encoding.decodeSystemWriter(oplog.key, oplog.value)
406
441
 
407
- const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
408
- if (!length) continue
442
+ batch = []
409
443
 
410
- const id = b4a.toString(key, 'hex')
411
- const current = seen.get(id) || 0
444
+ if (!length) continue
412
445
 
413
- if (current >= length) continue
446
+ const id = b4a.toString(key, 'hex')
447
+ const current = seen.get(id) || 0
448
+
449
+ if (current >= length) continue
450
+
451
+ seen.set(id, length)
452
+ nodes.push({ key, length })
453
+ continue
454
+ }
414
455
 
415
- seen.set(id, length)
416
- nodes.push({ key, length })
456
+ if (entry.key[0] === 0x01) batch.push(blk)
417
457
  }
418
458
 
419
459
  return nodes
package/lib/system.js CHANGED
@@ -8,8 +8,6 @@ const topo = require('./topo.js')
8
8
 
9
9
  const INFO_KEY = b4a.from([0])
10
10
  const INFO_LEGACY_KEY = b4a.concat([b4a.from([0, 0]), b4a.from('info')])
11
- const INDEXER_GTE = b4a.from([2])
12
- const INDEXER_LT = b4a.from([3])
13
11
  const EMPTY_HEAD = { length: 0, key: null }
14
12
  const EMPTY = b4a.from([])
15
13
 
@@ -124,13 +122,6 @@ module.exports = class Systembee {
124
122
  return null
125
123
  }
126
124
 
127
- async *getIndexers() {
128
- for await (const data of this.bee.createReadStream({ gte: INDEXER_GTE, lt: INDEXER_LT })) {
129
- const key = data.key.subarray(1)
130
- yield key
131
- }
132
- }
133
-
134
125
  async reset({ timeout } = {}) {
135
126
  const info = await this.getInfo({ timeout })
136
127
 
package/lib/topo.js CHANGED
@@ -76,7 +76,7 @@ async function sort(auto, batch) {
76
76
  for await (const data of ch) {
77
77
  if (isLegacyNode(data)) break
78
78
 
79
- const { ref } = getOplog(data)
79
+ const ref = getOplog(data)
80
80
  const link = {
81
81
  key: ref.oplog.key,
82
82
  length: ref.oplog.length,
@@ -318,23 +318,18 @@ async function* oplogIterator(bee) {
318
318
  }
319
319
 
320
320
  function getOplog(data) {
321
- const result = { ref: null, info: null }
322
321
  for (const { keys } of data.batch) {
322
+ if (!keys) continue
323
+
323
324
  for (const k of keys) {
324
325
  const prefix = k.key[0]
325
326
 
326
- if (prefix === 0) {
327
- result.info = encoding.decodeSystemInfo(k.value)
328
- if (result.ref) return result
329
- }
330
-
331
327
  if (prefix === 1) {
332
328
  // expect inlined for now
333
329
  const value = encoding.decodeSystemWriter(k.key, k.value)
334
330
  if (!value.isOplog) continue
335
331
 
336
- result.ref = { undo: data.tail || EMPTY_HEAD, oplog: value }
337
- if (result.info) return result
332
+ return { undo: data.tail || EMPTY_HEAD, oplog: value }
338
333
  }
339
334
  }
340
335
  }
@@ -395,6 +390,7 @@ function isLinkingAll(node, heads) {
395
390
 
396
391
  function isLegacyNode(data) {
397
392
  for (const { keys } of data.batch) {
393
+ if (!keys) continue
398
394
  for (const k of keys) {
399
395
  if (b4a.equals(k.key, INFO_LEGACY_KEY)) return true
400
396
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",