autobee 2.0.0-rc.1 → 2.0.0-rc.11

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