autobee 2.0.0-rc.0 → 2.0.0-rc.10

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
@@ -105,6 +105,8 @@ module.exports = class Autobee extends ReadyResource {
105
105
  this._hasApply = !!handlers.apply
106
106
  this._hasUpdate = !!handlers.update
107
107
  this._needsUpdate = false
108
+ this._ackRequired = false
109
+ this._ackedHeads = new Map()
108
110
  this._updateLocalCore = null
109
111
  this._host = new ApplyCalls(this)
110
112
  this._notifyHandler = null
@@ -227,6 +229,7 @@ module.exports = class Autobee extends ReadyResource {
227
229
  await this.local.close()
228
230
  await this.system.close()
229
231
  await this._wakeup.close()
232
+ if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
230
233
  await this._workingBee.close()
231
234
  await this.bee.close()
232
235
  await this.store.close()
@@ -250,7 +253,12 @@ module.exports = class Autobee extends ReadyResource {
250
253
  view: info ? info.view : EMPTY_HEAD
251
254
  }
252
255
 
253
- const shared = await this.system.commonAncestor(system || EMPTY_HEAD)
256
+ // no common ancestor (null) -> flushes -1 never matches, forcing a full diff
257
+ const shared = (await this.system.commonAncestor(system || EMPTY_HEAD)) || {
258
+ flushes: -1,
259
+ system: EMPTY_HEAD,
260
+ view: EMPTY_HEAD
261
+ }
254
262
 
255
263
  return UpdateChanges.from(shared, current)
256
264
  }
@@ -374,13 +382,18 @@ module.exports = class Autobee extends ReadyResource {
374
382
 
375
383
  // Use the view position from the system info (authoritative, post-processing)
376
384
  // rather than from the oplog (stale, captured at append time before _bump)
377
- let view = this.system.view || EMPTY_HEAD
385
+ let view = this.system.view
386
+ if (!view) {
387
+ const migrated = await this.local.getUserData('autobee/migrated-view')
388
+ view = migrated ? encoding.decodeBootRecord(migrated) : EMPTY_HEAD
389
+ }
378
390
 
379
391
  // @todo migration
380
392
  if (result.migration) {
381
393
  if (this.handlers.migrate) {
382
394
  view = (await this.handlers.migrate(result.migration.views)) || EMPTY_HEAD
383
395
  this._catchupMigratedNodes = result.migration.catchup
396
+ await this._storeMigratedView(view)
384
397
  }
385
398
 
386
399
  // ff boot invalidated by migration
@@ -476,6 +489,8 @@ module.exports = class Autobee extends ReadyResource {
476
489
  }
477
490
 
478
491
  async _drain() {
492
+ if (this._updating) await this._updating
493
+
479
494
  if (this.bootFrom) {
480
495
  await this._initFromHead(this.bootFrom)
481
496
  this.bootFrom = null
@@ -484,6 +499,8 @@ module.exports = class Autobee extends ReadyResource {
484
499
  // tracked across drain iteration
485
500
  this.system.shared = null
486
501
 
502
+ this._ackRequired = false
503
+
487
504
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
488
505
  if (changes) changes.track()
489
506
 
@@ -503,7 +520,12 @@ module.exports = class Autobee extends ReadyResource {
503
520
  break // revaluate conditions...
504
521
  }
505
522
 
506
- if (!(await this._bumpPendingWriters())) break
523
+ if (await this._bumpPendingWriters()) {
524
+ this._needsUpdate = true
525
+ continue
526
+ }
527
+
528
+ if (!this._appendAck()) break
507
529
  this._needsUpdate = true
508
530
  }
509
531
 
@@ -518,7 +540,15 @@ module.exports = class Autobee extends ReadyResource {
518
540
  if (this._interrupting) return
519
541
 
520
542
  if (this._needsUpdate) {
521
- await this._update(changes)
543
+ const updating = rrp()
544
+ this._updating = updating.promise
545
+
546
+ try {
547
+ await this._update(changes)
548
+ } finally {
549
+ this._updating = null
550
+ updating.resolve()
551
+ }
522
552
  }
523
553
  }
524
554
 
@@ -564,58 +594,74 @@ module.exports = class Autobee extends ReadyResource {
564
594
  }
565
595
  }
566
596
 
567
- async _getOplog(key, length) {
597
+ async _getOplog(key, length, opts = null) {
568
598
  const core = this.openCore(key)
569
- await core.ready()
570
599
 
571
- const target = length >= 0 ? length : core.length
572
- if (target === 0) return null
600
+ try {
601
+ await core.ready()
573
602
 
574
- const buf = await core.get(target - 1)
575
- let op = encoding.decodeOplog(buf)
603
+ const target = length >= 0 ? length : core.length
604
+ if (target === 0) return null
576
605
 
577
- if (op.version < 3) {
578
- op = await this._inflateLegacyOplog(buf, core, target - 1)
579
- }
606
+ const buf = await core.get(target - 1, opts)
607
+ if (buf === null) return null
580
608
 
581
- await core.close()
609
+ let op = encoding.decodeOplog(buf)
582
610
 
583
- if (buf === null) return null
611
+ // legacy nodes always inflate, but only indexers carry views - callers
612
+ // that need system info must check op.views
613
+ if (op.version < 3) {
614
+ op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
615
+ }
584
616
 
585
- return {
586
- key,
587
- length: target,
588
- op
617
+ return {
618
+ key,
619
+ length: target,
620
+ op
621
+ }
622
+ } finally {
623
+ await core.close()
589
624
  }
590
625
  }
591
626
 
592
- async _inflateLegacyOplog(buf, core, seq) {
627
+ async _inflateLegacyOplog(buf, core, seq, opts = null) {
593
628
  const m = encoding.decodeRawOplog(buf)
629
+
630
+ const op = {
631
+ version: m.version,
632
+ timestamp: 0,
633
+ links: m.node.heads,
634
+ batch: { start: 0, end: m.node.batch - 1 },
635
+ views: null,
636
+ optimistic: !!m.optimistic,
637
+ value: m.node.value
638
+ }
639
+
640
+ if (m.digest === null || m.checkpoint === null) return op
641
+
594
642
  const fetches = []
595
643
 
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)
644
+ fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
645
+ fetches.push(
646
+ m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
647
+ )
598
648
 
599
649
  const [digestNode, checkpointNode] = await Promise.all(fetches)
650
+ // a wait: false get we could not satisfy locally
651
+ if (digestNode === null || checkpointNode === null) return op
600
652
 
601
653
  const { digest } = encoding.decodeRawOplog(digestNode)
602
654
  const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
603
655
 
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
656
+ op.views = {
657
+ system: {
658
+ key: digest.key,
659
+ length: checkpoint.system.checkpoint.length
615
660
  },
616
- optimistic: !!m.optimistic,
617
- value: m.node.value
661
+ flushes: seq
618
662
  }
663
+
664
+ return op
619
665
  }
620
666
 
621
667
  async _update(changes) {
@@ -744,6 +790,39 @@ module.exports = class Autobee extends ReadyResource {
744
790
  }
745
791
  }
746
792
 
793
+ // append a null value node to ack writer
794
+ _appendAck() {
795
+ if (!this._ackRequired) return false
796
+ if (!this.writers.writable) return false
797
+
798
+ if (this.writers.localWriter.pending !== null) return false
799
+
800
+ const links = this.system.getLinks(this.local.key)
801
+ const t = Math.max(this._now(), this.system.timestamp)
802
+
803
+ let unlinked = false
804
+ for (const { key, length } of links) {
805
+ const hex = b4a.toString(key, 'hex')
806
+ const acked = this._ackedHeads.get(hex) || 0
807
+ if (length > acked) {
808
+ unlinked = true
809
+ break
810
+ }
811
+ }
812
+ if (!unlinked) {
813
+ this._ackRequired = false
814
+ return false
815
+ }
816
+
817
+ for (const { key, length } of links) {
818
+ this._ackedHeads.set(b4a.toString(key, 'hex'), length)
819
+ }
820
+
821
+ this.writers.appendLocal(null, t, { start: 0, end: 0 }, links, false, null)
822
+ this._ackRequired = false
823
+ return true
824
+ }
825
+
747
826
  async _bumpPendingWriters() {
748
827
  if (this._catchupMigratedNodes !== null) {
749
828
  await this._bumpMigratedWriters()
@@ -917,11 +996,17 @@ module.exports = class Autobee extends ReadyResource {
917
996
  const boot = this.system.bootRecord()
918
997
  if (boot) {
919
998
  proms.push(this.local.setUserData('autobee/head', encoding.encodeBootRecord(boot)))
999
+ if (this.system.view) proms.push(this._storeMigratedView(EMPTY_HEAD))
920
1000
  }
921
1001
 
922
1002
  return Promise.all(proms)
923
1003
  }
924
1004
 
1005
+ _storeMigratedView(view) {
1006
+ const value = view.key ? encoding.encodeBootRecord(view) : null
1007
+ return this.local.setUserData('autobee/migrated-view', value)
1008
+ }
1009
+
925
1010
  static decodeValue(buf, opts) {
926
1011
  return encoding.decodeValue(buf, opts)
927
1012
  }
@@ -1071,14 +1156,16 @@ module.exports = class Autobee extends ReadyResource {
1071
1156
 
1072
1157
  async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
1073
1158
  const oplog = await this._getOplog(head.key, head.length)
1074
- if (!oplog) return false
1159
+ if (!oplog) return null
1075
1160
 
1076
1161
  const verified = trusted ? await this._getOplog(trusted.key, trusted.length) : oplog
1162
+ if (!verified) return null
1077
1163
 
1078
- if (!verified.op.views) return null
1164
+ // legacy nodes from non-indexers have no system info to reboot from
1165
+ if (!oplog.op.views || !verified.op.views) return null
1079
1166
 
1080
1167
  if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
1081
- return false
1168
+ return null
1082
1169
  }
1083
1170
 
1084
1171
  const moved = await this._moveTo(batchToHead(verified.op.views.system), {
@@ -1145,12 +1232,16 @@ module.exports = class Autobee extends ReadyResource {
1145
1232
  this.system.bee.move(head)
1146
1233
  await this.system.reset()
1147
1234
 
1148
- // todo: binary search to find shared common tree and pass as undo
1149
- this.system.shared = { flushes: -1, view: EMPTY_HEAD, system: EMPTY_HEAD }
1235
+ this.system.shared = this.rebootTo.shared || {
1236
+ flushes: -1,
1237
+ view: EMPTY_HEAD,
1238
+ system: EMPTY_HEAD
1239
+ }
1150
1240
 
1151
1241
  // migrate is set when fast-forwarding from a legacy head
1152
1242
  if (migrate) {
1153
1243
  const view = (await this.handlers.migrate(migrate)) || EMPTY_HEAD
1244
+ await this._storeMigratedView(view)
1154
1245
  this.bee.move(view)
1155
1246
  this._workingBee.move(view)
1156
1247
  } else {
@@ -1161,6 +1252,8 @@ module.exports = class Autobee extends ReadyResource {
1161
1252
  this.rebootTo = null
1162
1253
  await this.writers.refresh()
1163
1254
 
1255
+ await this._storeBoot()
1256
+
1164
1257
  await this._update(changes)
1165
1258
 
1166
1259
  this.emit('move-to', to, from)
@@ -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/reboot.js CHANGED
@@ -86,10 +86,23 @@ module.exports = class Reboot {
86
86
  await Promise.all(promises)
87
87
  if (this.destroyed) return null
88
88
 
89
+ const shared = await this._commonAncestor()
90
+ if (this.destroyed) return null
91
+
89
92
  return {
90
93
  head: this.head,
91
94
  tip: this.tip,
92
- migrate: null
95
+ migrate: null,
96
+ shared
97
+ }
98
+ }
99
+
100
+ async _commonAncestor() {
101
+ try {
102
+ return await this.auto.system.commonAncestor(this.head)
103
+ } catch (err) {
104
+ safetyCatch(err)
105
+ return null
93
106
  }
94
107
  }
95
108
 
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,44 +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
- a = await hist1.next()
127
- if (a.value) l = getInfo(a.value)
128
- }
161
+ const lf = l.info.flushes
162
+ const rf = r.info.flushes
129
163
 
130
- if (r.flushes >= l.flushes) {
131
- b = await hist2.next()
132
- if (b.value) r = getInfo(b.value)
133
- }
164
+ if (lf >= rf) l = await next(ca)
165
+ if (rf >= lf) r = await next(cb)
134
166
  }
135
167
 
136
- return { flushes: 0, system: EMPTY_HEAD, view: EMPTY_HEAD }
168
+ // no common ancestor
169
+ return null
137
170
  }
138
171
 
139
172
  async reset({ timeout } = {}) {
@@ -228,6 +261,14 @@ module.exports = class Systembee {
228
261
  return info ? !info.isRemoved : !!optimistic
229
262
  }
230
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
+
231
272
  async get(key, { unflushed = false, timeout } = {}) {
232
273
  const node = await this.bee.get(encoding.encodeSystemWriterKey(key), { timeout })
233
274
  const info = node !== null ? encoding.decodeSystemWriter(node.key, node.value) : null
@@ -434,6 +475,8 @@ module.exports = class Systembee {
434
475
 
435
476
  w.tryPut(INFO_KEY, encoding.encodeSystemInfo(info))
436
477
 
478
+ this.view = info.view
479
+
437
480
  await w.flush()
438
481
 
439
482
  // Can enable if needed during debuggin
@@ -460,12 +503,16 @@ function getInfo(data) {
460
503
  const prefix = k.key[0]
461
504
 
462
505
  if (prefix === 0) {
463
- return encoding.decodeSystemInfo(k.value)
506
+ try {
507
+ return encoding.decodeSystemInfo(k.value)
508
+ } catch {
509
+ return null
510
+ }
464
511
  }
465
512
  }
466
513
  }
467
514
 
468
- asserts.bail('Bad system node')
515
+ return null
469
516
  }
470
517
 
471
518
  function sameHead(a, b) {
@@ -473,3 +520,35 @@ function sameHead(a, b) {
473
520
  if (!a.key || !b.key) return a.key === b.key
474
521
  return b4a.equals(a.key, b.key)
475
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() {
@@ -65,17 +66,66 @@ async function rollback(auto, system, verified) {
65
66
  }
66
67
  }
67
68
 
69
+ // nodes we cannot resolve are returned as null rather than dropped, so callers
70
+ // can tell an incomplete replay from a short one
68
71
  async function replay(auto) {
69
- const ch = auto.system.bee.createChangesStream()
70
72
  const replay = []
71
73
 
72
- for await (const data of ch) {
73
- const { ref } = getOplog(data)
74
- replay.push(ref)
74
+ for await (const entry of oplogIterator(auto.system.bee)) {
75
+ // the walk was cut short, nothing below this point is reachable
76
+ if (entry === null) {
77
+ replay.push(null)
78
+ break
79
+ }
80
+
81
+ for (const ref of entry.refs) replay.push(ref)
75
82
  }
76
83
 
77
- const inflated = await inflate(auto, replay.reverse())
78
- return inflated.flatMap((b) => b.batch)
84
+ // same reasoning as the iterator - never block on what we do not have
85
+ const inflated = await inflate(auto, replay.reverse(), { wait: false })
86
+ return inflated.flatMap((b) => (b === null ? [null] : b.batch))
87
+ }
88
+
89
+ // tolerates legacy nodes instead of asserting
90
+ async function* oplogIterator(bee) {
91
+ try {
92
+ for await (const node of bee.createChangesStream({ wait: false })) {
93
+ const legacy = isLegacyNode(node)
94
+ const refs = getOplogs(node)
95
+ if (refs.length) yield { node, refs, legacy }
96
+ }
97
+ } catch {
98
+ yield null // signal data not available
99
+ }
100
+ }
101
+
102
+ // legacy nodes carry no isOplog flag so we cannot tell which writer update is
103
+ // the oplog to fetch - yield every candidate until we have a better signal
104
+ function getOplogs(data) {
105
+ const undo = data.tail || EMPTY_HEAD
106
+ const oplogs = []
107
+ const candidates = []
108
+
109
+ for (const { keys } of data.batch) {
110
+ for (const k of keys) {
111
+ if (k.key[0] !== 1) continue
112
+
113
+ let value = null
114
+ try {
115
+ value = encoding.decodeSystemWriter(k.key, k.value)
116
+ } catch {
117
+ continue
118
+ }
119
+
120
+ // writer was just added, nothing to fetch
121
+ if (!value.length) continue
122
+
123
+ if (value.isOplog) oplogs.push({ undo, oplog: value })
124
+ else candidates.push({ undo, oplog: value })
125
+ }
126
+ }
127
+
128
+ return oplogs.length ? oplogs : candidates
79
129
  }
80
130
 
81
131
  async function sort(auto, batch) {
@@ -192,12 +242,19 @@ function cmp(a, b) {
192
242
  return a.length < b.length ? -1 : 1
193
243
  }
194
244
 
195
- async function inflate(auto, reorder) {
245
+ async function inflate(auto, reorder, opts = null) {
196
246
  const promises = []
197
247
  const sessions = new Map()
198
248
 
199
249
  for (let i = 0; i < reorder.length; i++) {
200
250
  const ref = reorder[i]
251
+
252
+ // marker for a node we could not resolve, pass it through
253
+ if (ref === null) {
254
+ promises.push(null)
255
+ continue
256
+ }
257
+
201
258
  const id = b4a.toString(ref.oplog.key, 'hex')
202
259
  // only seeds the node - reapplication re-resolves from the witness
203
260
  const weight = ref.oplog.weight
@@ -214,7 +271,7 @@ async function inflate(auto, reorder) {
214
271
  sessions.set(id, core)
215
272
  }
216
273
 
217
- promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i))
274
+ promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, opts))
218
275
  }
219
276
 
220
277
  try {
@@ -224,9 +281,10 @@ async function inflate(auto, reorder) {
224
281
  }
225
282
  }
226
283
 
227
- async function getOplogBatch(undo, core, length, weight, index) {
284
+ async function getOplogBatch(undo, core, length, weight, index, opts = null) {
228
285
  const seq = length - 1
229
- const block = await core.get(seq)
286
+ const block = await core.get(seq, opts)
287
+ if (block === null) return null
230
288
  const oplog = encoding.decodeOplog(block)
231
289
  const head = createNode(core, length, weight, oplog)
232
290
 
@@ -238,12 +296,18 @@ async function getOplogBatch(undo, core, length, weight, index) {
238
296
  const end = seq + head.batch.end // skip head, we have it
239
297
 
240
298
  for (let i = start; i < end; i++) {
241
- remaining.push(core.get(i))
299
+ remaining.push(core.get(i, opts))
242
300
  }
243
301
 
244
302
  if (remaining.length) {
245
303
  const blocks = await Promise.all(remaining)
246
304
  for (let i = 0; i < blocks.length; i++) {
305
+ // keep the hole so the gap in the batch stays visible
306
+ if (blocks[i] === null) {
307
+ batch.push(null)
308
+ continue
309
+ }
310
+
247
311
  const oplog = encoding.decodeOplog(blocks[i])
248
312
  batch.push(createNode(core, i + start + 1, weight, oplog))
249
313
  }
@@ -254,8 +318,12 @@ async function getOplogBatch(undo, core, length, weight, index) {
254
318
  }
255
319
 
256
320
  function getLocalBatch(undo, writer, length, weight, index) {
321
+ if (writer.core.closed) throw new Error('Writer core is closed')
322
+
257
323
  if (length > writer.core.length) {
258
- if (!writer.pending) throw new Error('No local nodes')
324
+ if (!writer.pending) {
325
+ throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}`)
326
+ }
259
327
 
260
328
  let i = 0
261
329
  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.0",
3
+ "version": "2.0.0-rc.10",
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",