autobee 2.0.0-rc.2 → 2.0.0-rc.21

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,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
 
@@ -0,0 +1,25 @@
1
+ module.exports = class ApplyView {
2
+ constructor(bee, auto) {
3
+ this.bee = bee
4
+ this.auto = auto
5
+ this.view = ApplyView.open(bee, auto)
6
+ }
7
+
8
+ static open(bee, auto) {
9
+ return auto._handlers.open ? auto._handlers.open(bee, auto) : bee
10
+ }
11
+
12
+ static close(view, auto) {
13
+ if (!auto._handlers.close) return Promise.resolve()
14
+ return auto._handlers.close(view)
15
+ }
16
+
17
+ apply(userBatch) {
18
+ return this.auto._handlers.apply(userBatch, this.view, this.auto._host)
19
+ }
20
+
21
+ async close() {
22
+ await ApplyView.close(this.view, this.auto)
23
+ await this.bee.close()
24
+ }
25
+ }
package/lib/boot.js CHANGED
@@ -201,6 +201,8 @@ async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
201
201
  mappedViews.set(name, v)
202
202
  }
203
203
 
204
+ await core.close()
205
+
204
206
  return {
205
207
  encryptionKey,
206
208
  system,
@@ -242,6 +244,7 @@ async function getCatchupHeads(core) {
242
244
  if (entry.key[0] !== 0x01) continue
243
245
 
244
246
  const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
247
+ if (!length) continue
245
248
  nodes.push({ key, length })
246
249
  }
247
250
 
@@ -254,7 +257,9 @@ async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
254
257
  const core = store.get(head.key)
255
258
  await core.ready()
256
259
 
257
- await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
260
+ if (encryptionKey) {
261
+ await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
262
+ }
258
263
 
259
264
  let seq = head.length - 1
260
265
  const block = await core.get(seq--, { wait: false })
package/lib/encoding.js CHANGED
@@ -12,6 +12,7 @@ const SystemInfo = getEncoding('@autobee/system-info')
12
12
  const SystemWriter = getEncoding('@autobee/system-writer')
13
13
  const ManifestData = getEncoding('@autobee/manifest-data')
14
14
  const AutobaseBootRecord = getEncoding('@autobase-compat/boot-record')
15
+ const MigratedHead = getEncoding('@autobee/migrated-head')
15
16
 
16
17
  exports.OPLOG_VERSION = OPLOG_VERSION
17
18
 
@@ -21,6 +22,9 @@ exports.ManifestData = ManifestData
21
22
  exports.encodeBootRecord = encodeBootRecord
22
23
  exports.decodeBootRecord = decodeBootRecord
23
24
 
25
+ exports.encodeMigratedHead = encodeMigratedHead
26
+ exports.decodeMigratedHead = decodeMigratedHead
27
+
24
28
  exports.encodePreviousDrain = encodePreviousDrain
25
29
  exports.decodePreviousDrain = decodePreviousDrain
26
30
 
@@ -39,6 +43,14 @@ function decodeBootRecord(value) {
39
43
  return c.decode(Link, value)
40
44
  }
41
45
 
46
+ function encodeMigratedHead(m) {
47
+ return c.encode(MigratedHead, m)
48
+ }
49
+
50
+ function decodeMigratedHead(value) {
51
+ return c.decode(MigratedHead, value)
52
+ }
53
+
42
54
  function encodePreviousDrain(d) {
43
55
  return c.encode(c.uint, d)
44
56
  }
@@ -1,5 +1,6 @@
1
1
  const safetyCatch = require('safety-catch')
2
2
  const b4a = require('b4a')
3
+ const Hypercore = require('hypercore')
3
4
  const c = require('compact-encoding')
4
5
  const crypto = require('hypercore-crypto')
5
6
 
@@ -18,13 +19,24 @@ const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
18
19
 
19
20
  const EMPTY = b4a.alloc(0)
20
21
  const DEFAULT_OP_TIMEOUT = 5_000
22
+ const MIN_FF_GAP = 32
21
23
 
22
- module.exports = class Reboot {
24
+ // views are only stamped on the last node of a flush, so a head may sit above
25
+ // the most recent one - how far back we look for it
26
+ const MAX_FLUSH_SEARCH = 64
27
+
28
+ module.exports = class FastForward {
23
29
  constructor(auto, head, tip, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
24
30
  this.auto = auto
31
+
32
+ this.activeRequests = []
33
+
34
+ // 'reboot' is a storage namespace: renaming it would derive a different
35
+ // scratch core on disk, so it stays as is
25
36
  this.system = new System(auto.store.namespace('reboot'), null, {
26
37
  getEncryptionProvider: this.auto.getSystemEncryption,
27
- encrypted: this.encrypted
38
+ encrypted: this.encrypted,
39
+ activeRequests: this.activeRequests
28
40
  })
29
41
 
30
42
  // length -1 marks an uninitialised head: we know the key but not the length
@@ -37,6 +49,164 @@ module.exports = class Reboot {
37
49
  this.cores = []
38
50
  }
39
51
 
52
+ static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
53
+
54
+ static async fromHead(
55
+ auto,
56
+ head,
57
+ trusted,
58
+ { force = false, timeout = 0, condition = null, reference = null } = {}
59
+ ) {
60
+ const conservative = !force && auto._conservativeFF
61
+ const timeoutOpts = timeout ? { timeout } : null
62
+
63
+ const oplog = await FastForward.flushHead(auto, head, {
64
+ conservative,
65
+ ...timeoutOpts
66
+ })
67
+ if (!oplog) return null
68
+
69
+ const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
70
+ if (!verified) return null
71
+
72
+ // legacy nodes from non-indexers have no system info to fast-forward from
73
+ if (!oplog.op.views || !verified.op.views) return null
74
+
75
+ if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
76
+ return null
77
+ }
78
+
79
+ if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
80
+ return null
81
+ }
82
+
83
+ const tip = {
84
+ system: batchToHead(oplog.op.views.system),
85
+ verified: {
86
+ op: { key: verified.key, length: verified.length },
87
+ flushes: verified.op.views.flushes
88
+ }
89
+ }
90
+
91
+ return new FastForward(auto, batchToHead(verified.op.views.system), tip)
92
+ }
93
+
94
+ static async fromHeads(
95
+ auto,
96
+ heads,
97
+ { force = false, timeout = 0, condition = null, reference = null } = {}
98
+ ) {
99
+ if (reference === null) reference = auto._workingView.view
100
+
101
+ const promises = []
102
+
103
+ for (const head of heads) {
104
+ if (head.length === 0) continue
105
+ promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
106
+ }
107
+
108
+ const ops = await Promise.all(promises)
109
+ if (auto.fastForwarding || auto.fastForwardTo) return null
110
+
111
+ const trust = await Promise.all(
112
+ ops.map((res) => {
113
+ return res === null ? false : auto.trusted.isTrusted(res.key, reference)
114
+ })
115
+ )
116
+ if (auto.fastForwarding || auto.fastForwardTo) return null
117
+
118
+ const candidates = []
119
+
120
+ let bestTrusted = null
121
+ let bestTrustedFlushes = -1
122
+
123
+ for (let i = 0; i < ops.length; i++) {
124
+ const res = ops[i]
125
+ if (res === null || !res.op.views) continue
126
+ if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
127
+
128
+ if (!trust[i]) {
129
+ candidates.push(res)
130
+ continue
131
+ }
132
+
133
+ if (res.op.views.flushes > bestTrustedFlushes) {
134
+ bestTrustedFlushes = res.op.views.flushes
135
+ bestTrusted = res
136
+ }
137
+ }
138
+
139
+ if (bestTrusted !== null) {
140
+ return FastForward.fromHead(auto, bestTrusted, null, {
141
+ force,
142
+ timeout,
143
+ condition,
144
+ reference
145
+ })
146
+ }
147
+
148
+ candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
149
+
150
+ for (const res of candidates) {
151
+ const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
152
+ if (trusted === null) continue
153
+ if (auto.fastForwarding || auto.fastForwardTo) return null
154
+
155
+ const ff = await FastForward.fromHead(auto, res, trusted, {
156
+ force,
157
+ timeout,
158
+ condition,
159
+ reference
160
+ })
161
+ if (ff !== null) return ff
162
+ }
163
+
164
+ return null
165
+ }
166
+
167
+ static async flushHead(auto, head, opts) {
168
+ const core = auto.openCore(head.key)
169
+
170
+ try {
171
+ let length = head.length
172
+
173
+ for (let i = 0; i < MAX_FLUSH_SEARCH; i++) {
174
+ const oplog = await auto.readOplog(core, length, opts)
175
+ if (oplog === null) return null
176
+ if (oplog.op.views) return oplog
177
+
178
+ length = oplog.length - 1
179
+ if (length <= 0) return null
180
+ }
181
+
182
+ return null
183
+ } finally {
184
+ await core.close()
185
+ }
186
+ }
187
+
188
+ static async accepts(auto, oplog, condition, reference) {
189
+ const opened = auto.openViewAt(oplog)
190
+ if (opened === null) return false
191
+
192
+ try {
193
+ return !!(await condition(opened.view, reference))
194
+ } finally {
195
+ await opened.close()
196
+ }
197
+ }
198
+
199
+ static async mostRecentTrusted(auto, head, reference) {
200
+ const opened = auto.openViewAt(head)
201
+ if (opened === null) return null
202
+
203
+ try {
204
+ return (await auto.trusted.mostRecentTrusted(opened.view, reference)) || null
205
+ } finally {
206
+ await opened.close()
207
+ }
208
+ }
209
+
40
210
  async run() {
41
211
  try {
42
212
  if (!this.running) this.running = this._run()
@@ -64,8 +234,15 @@ module.exports = class Reboot {
64
234
 
65
235
  // a legacy system needs migrating rather than a plain fast-forward
66
236
  if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
67
- // already migrated, don't migrate again
68
- if (this.auto.system.version > LEGACY_AUTOBASE_VERSION) return null
237
+ const migrated = this.auto._migratedHead && this.auto._migratedHead.system
238
+ if (
239
+ migrated &&
240
+ b4a.equals(migrated.key, this.head.key) &&
241
+ migrated.length >= this.head.length
242
+ ) {
243
+ return null
244
+ }
245
+
69
246
  return this._migrate()
70
247
  }
71
248
 
@@ -77,7 +254,12 @@ module.exports = class Reboot {
77
254
  const view = this.auto.store.get({ key: this.system.view.key, active: true })
78
255
  this.cores.push(view)
79
256
 
80
- promises.push(view.get(this.system.view.length - 1, { timeout: this.timeout }))
257
+ promises.push(
258
+ view.get(this.system.view.length - 1, {
259
+ timeout: this.timeout,
260
+ activeRequests: this.activeRequests
261
+ })
262
+ )
81
263
 
82
264
  for (const head of this.system.heads) {
83
265
  promises.push(this.system.get(head.key, { timeout: this.timeout }))
@@ -86,23 +268,12 @@ module.exports = class Reboot {
86
268
  await Promise.all(promises)
87
269
  if (this.destroyed) return null
88
270
 
89
- const shared = await this._commonAncestor()
90
271
  if (this.destroyed) return null
91
272
 
92
273
  return {
93
274
  head: this.head,
94
275
  tip: this.tip,
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
276
+ migrate: null
106
277
  }
107
278
  }
108
279
 
@@ -118,7 +289,12 @@ module.exports = class Reboot {
118
289
  await core.setEncryption(this.auto.getSystemEncryption())
119
290
 
120
291
  // Decode hyperbee block using hyperbee2 compat
121
- const node = decodeBlock(await core.get(this.head.length - 1, { timeout: this.timeout }))
292
+ const node = decodeBlock(
293
+ await core.get(this.head.length - 1, {
294
+ timeout: this.timeout,
295
+ activeRequests: this.activeRequests
296
+ })
297
+ )
122
298
  assert(node.keys.length > 0, 'bad system block')
123
299
 
124
300
  // Decode system info from hyperbee block using autobee compat
@@ -163,7 +339,12 @@ module.exports = class Reboot {
163
339
  await core.ready()
164
340
 
165
341
  try {
166
- if (!core.manifest) await core.get(length - 1, { timeout: this.timeout })
342
+ if (!core.manifest) {
343
+ await core.get(length - 1, {
344
+ timeout: this.timeout,
345
+ activeRequests: this.activeRequests
346
+ })
347
+ }
167
348
  } catch {
168
349
  return null
169
350
  }
@@ -191,13 +372,26 @@ module.exports = class Reboot {
191
372
  }
192
373
  }
193
374
 
375
+ // cancels every read in flight and fails any that come after
376
+ clearRequests(err = null) {
377
+ Hypercore.destroyRequests(this.activeRequests, err)
378
+ }
379
+
194
380
  async close() {
195
381
  this.destroyed = true
382
+ this.clearRequests()
196
383
  if (this.system) await this.system.close()
197
384
  for (const core of this.cores) await core.close()
198
385
  }
199
386
  }
200
387
 
388
+ function batchToHead(b) {
389
+ return {
390
+ key: b.key,
391
+ length: b.start + b.length
392
+ }
393
+ }
394
+
201
395
  function decodeLegacySystemInfo(buffer) {
202
396
  const state = { start: 0, end: buffer.length, buffer }
203
397
  const version = c.uint.decode(state)
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
@@ -26,7 +46,6 @@ module.exports = class Systembee {
26
46
  this.indexers = null // legacy
27
47
  this.updates = new Map()
28
48
  this.writers = new Map()
29
- this.shared = null
30
49
  this.encrypted = opts.encrypted === true
31
50
  }
32
51
 
@@ -95,49 +114,6 @@ module.exports = class Systembee {
95
114
  }
96
115
  }
97
116
 
98
- async commonAncestor(head) {
99
- const info = await this.getInfo()
100
-
101
- const snap = this.bee.checkout(head)
102
-
103
- const hist1 = this.bee.createChangesStream()[Symbol.asyncIterator]()
104
- const hist2 = snap.createChangesStream()[Symbol.asyncIterator]()
105
-
106
- let a = await hist1.next()
107
- let b = await hist2.next()
108
-
109
- if (!a.value || !b.value) {
110
- return { flushes: 0, system: EMPTY_HEAD, view: EMPTY_HEAD }
111
- }
112
-
113
- let l = getInfo(a.value)
114
- let r = getInfo(b.value)
115
-
116
- while (!a.done || !b.done) {
117
- if (l.flushes === r.flushes && sameHead(l.view, r.view)) {
118
- return {
119
- flushes: l.flushes,
120
- system: a.value.head,
121
- view: l.view || EMPTY_HEAD
122
- }
123
- }
124
-
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
- }
130
-
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
- }
136
- }
137
-
138
- return { flushes: 0, system: EMPTY_HEAD, view: EMPTY_HEAD }
139
- }
140
-
141
117
  async reset({ timeout } = {}) {
142
118
  const info = await this.getInfo({ timeout })
143
119
 
@@ -147,7 +123,6 @@ module.exports = class Systembee {
147
123
  this.timestamp = info ? info.timestamp : 0
148
124
  this.flushes = info ? info.flushes : 0
149
125
  this.indexers = info ? info.indexers : null
150
- this.shared = null
151
126
  this.updates.clear()
152
127
  this.writers.clear()
153
128
 
@@ -169,16 +144,8 @@ module.exports = class Systembee {
169
144
  async undo(head) {
170
145
  this.bee.move(head)
171
146
 
172
- const shared = this.shared
173
147
  await this.reset()
174
148
 
175
- // deepest rewind since last mark - everything at or below it is untouched
176
- if (shared === null || this.flushes < shared.flushes) {
177
- this.shared = { flushes: this.flushes, view: this.view || EMPTY_HEAD, system: head }
178
- } else if (shared) {
179
- this.shared = shared
180
- }
181
-
182
149
  return this.view || EMPTY_HEAD
183
150
  }
184
151
 
@@ -230,6 +197,14 @@ module.exports = class Systembee {
230
197
  return info ? !info.isRemoved : !!optimistic
231
198
  }
232
199
 
200
+ snapshot(head = null) {
201
+ const bee =
202
+ head === null
203
+ ? this.bee.snapshot()
204
+ : this.bee.checkout({ length: head.length, key: head.key })
205
+ return new SystemSnapshot(bee)
206
+ }
207
+
233
208
  async get(key, { unflushed = false, timeout } = {}) {
234
209
  const node = await this.bee.get(encoding.encodeSystemWriterKey(key), { timeout })
235
210
  const info = node !== null ? encoding.decodeSystemWriter(node.key, node.value) : null
@@ -436,6 +411,9 @@ module.exports = class Systembee {
436
411
 
437
412
  w.tryPut(INFO_KEY, encoding.encodeSystemInfo(info))
438
413
 
414
+ this.version = info.version
415
+ this.view = info.view
416
+
439
417
  await w.flush()
440
418
 
441
419
  // Can enable if needed during debuggin
@@ -462,16 +440,14 @@ function getInfo(data) {
462
440
  const prefix = k.key[0]
463
441
 
464
442
  if (prefix === 0) {
465
- return encoding.decodeSystemInfo(k.value)
443
+ try {
444
+ return encoding.decodeSystemInfo(k.value)
445
+ } catch {
446
+ return null
447
+ }
466
448
  }
467
449
  }
468
450
  }
469
451
 
470
- asserts.bail('Bad system node')
471
- }
472
-
473
- function sameHead(a, b) {
474
- if (!a || !b || a.length !== b.length) return false
475
- if (!a.key || !b.key) return a.key === b.key
476
- return b4a.equals(a.key, b.key)
452
+ return null
477
453
  }