autobee 2.7.0 → 2.7.2

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/README.md CHANGED
@@ -256,13 +256,17 @@ Return the oplog head you most recently vouched for, given the `target` view bei
256
256
 
257
257
  Called at flush time to stamp your own oplog (with your view as `target` and a `null` reference), and again per candidate during discovery.
258
258
 
259
+ During discovery `target` is a view opened for the candidate, and reads through it carry the fast-forward's timeout. Anything else the hook awaits is unbounded, so if it reaches beyond that view, put a timeout on it: the candidate waits on the hook, and so does closing it.
260
+
259
261
  #### `warmup(view)`
260
262
 
261
263
  Prepare a candidate `view` before the fast-forward onto it is applied.
262
264
 
263
265
  Called as soon as the candidate's view head is known, and runs alongside the block downloads the fast-forward is already doing rather than after them. Throw or reject to reject the candidate: the fast-forward is abandoned and the usual apply path catches up instead.
264
266
 
265
- `view` is opened and closed through the `open` and `close` handlers, so don't build or close a db of your own.
267
+ `view` is opened and closed through the `open` and `close` handlers, so don't build or close a db of your own. Reads through it carry the fast-forward's timeout, so a warmup that needs blocks nobody serves fails the candidate instead of hanging.
268
+
269
+ Only reads through `view` are bounded. Anything else the handler awaits, such as other cores or your own network calls, is up to you to time out: the fast-forward waits on the handler, and an abandoned candidate cannot finish closing until it returns.
266
270
 
267
271
  #### `fastForward.boot`
268
272
 
@@ -281,9 +285,9 @@ Pass one of `head` or `legacy`, not both.
281
285
 
282
286
  #### `fastForward.conservative`
283
287
 
284
- Defaults to `true`: only fast-forward onto a head a connected peer can serve whole.
288
+ Defaults to `true`: only fast-forward once the system and view lengths the head stamps are held whole, either locally or by a connected peer.
285
289
 
286
- The check covers the oplog head only, not the system and view cores the fast-forward then reads.
290
+ The check runs at the end of the fast-forward, after the system has booted and the view head has been fetched, so a sparse view nobody can serve is skipped instead of landed on.
287
291
 
288
292
  #### `await db.moveTo(head, [options])`
289
293
 
package/index.js CHANGED
@@ -98,8 +98,8 @@ module.exports = class Autobee extends ReadyResource {
98
98
  // oplog head to boot from: migrates or fast-forwards depending on its version
99
99
  this.bootFrom = (fastForward && fastForward.boot) || null
100
100
 
101
- // conservative (default on): only fast-forward onto a head someone can serve whole
102
- this._conservativeFF = !fastForward || !!fastForward.conservative
101
+ // conservative (default on): only fast-forward onto a system and view held whole
102
+ this._conservativeFF = fastForward === null || fastForward.conservative !== false
103
103
 
104
104
  this.trusted = new TrustedPeers(this, handlers)
105
105
 
@@ -437,15 +437,17 @@ module.exports = class Autobee extends ReadyResource {
437
437
  }
438
438
 
439
439
  // the view an oplog head points at, opened and closed through the handlers
440
- openViewAt(oplog) {
440
+ openViewAt(oplog, { timeout } = {}) {
441
441
  const v = oplog.op.views.view
442
442
  if (!v) return null
443
443
 
444
- return this.openView({ key: v.key, length: v.start + v.length })
444
+ return this.openView({ key: v.key, length: v.start + v.length }, { timeout })
445
445
  }
446
446
 
447
- openView(head) {
448
- return new ApplyView(this.bee.checkout({ key: head.key, length: head.length }), this)
447
+ // timeout bounds every hypercore read done through the view, defaults to the main bee's
448
+ openView(head, { timeout } = {}) {
449
+ const bee = this.bee.checkout({ key: head.key, length: head.length, timeout })
450
+ return new ApplyView(bee, this)
449
451
  }
450
452
 
451
453
  openCore(key) {
@@ -826,7 +828,7 @@ module.exports = class Autobee extends ReadyResource {
826
828
 
827
829
  for (const { key, length } of hints) {
828
830
  if (length === 0) continue
829
- promises.push(this._resolveOplogHint(key, length, timeout ? { timeout } : null))
831
+ promises.push(this._resolveOplogHint(key, length, { timeout }))
830
832
  }
831
833
 
832
834
  const ops = await Promise.all(promises)
@@ -865,7 +867,7 @@ module.exports = class Autobee extends ReadyResource {
865
867
  }
866
868
  }
867
869
 
868
- async _getOplog(key, length, opts = null) {
870
+ async _getOplog(key, length, opts) {
869
871
  const core = this.openCore(key)
870
872
 
871
873
  try {
@@ -876,17 +878,12 @@ module.exports = class Autobee extends ReadyResource {
876
878
  }
877
879
 
878
880
  // reads on a core the caller owns, so a walk does not churn a session per step
879
- async readOplog(core, length, opts = null) {
881
+ async readOplog(core, length, { timeout = 0 } = {}) {
880
882
  await core.ready()
881
883
 
882
- const { conservative = false, timeout } = opts === null ? {} : opts
883
-
884
884
  const target = length >= 0 ? length : core.length
885
885
  if (target === 0) return null
886
886
 
887
- // conservative: only proceed when a connected peer can serve the head whole
888
- if (conservative && core.remoteContiguousLength < target) return null
889
-
890
887
  const buf = await core.get(target - 1, { timeout })
891
888
  if (buf === null) return null
892
889
 
@@ -1216,16 +1213,17 @@ module.exports = class Autobee extends ReadyResource {
1216
1213
  return t
1217
1214
  }
1218
1215
 
1219
- async applyBacklog(batches) {
1220
- const queue = batches.slice()
1216
+ async _processBatch(batch) {
1217
+ // a stack: the tip is pushed in reverse so it pops in order
1218
+ const stack = [batch]
1221
1219
 
1222
- while (queue.length) {
1223
- const batch = queue.shift()
1220
+ while (stack.length) {
1221
+ const batch = stack.pop()
1224
1222
  const t = await this.prepareBatch(batch)
1225
1223
 
1226
1224
  if (t.view) {
1227
1225
  this._workingBee.move(t.view)
1228
- queue.unshift(...t.tip)
1226
+ for (let i = t.tip.length - 1; i >= 0; i--) stack.push(t.tip[i])
1229
1227
  continue
1230
1228
  }
1231
1229
 
@@ -1235,15 +1233,12 @@ module.exports = class Autobee extends ReadyResource {
1235
1233
 
1236
1234
  await this._applyBatch(batch, batch[0].optimistic)
1237
1235
 
1238
- this.writers.triggers.trigger(
1239
- b4a.toString(batch[0].key, 'hex'),
1240
- batch[batch.length - 1].length
1241
- )
1242
- }
1243
- }
1236
+ // trigger any upstream writer waiting for this batch early
1237
+ const id = b4a.toString(batch[0].key, 'hex')
1238
+ const length = batch[batch.length - 1].length
1244
1239
 
1245
- async _processBatch(batch) {
1246
- await this.applyBacklog([batch])
1240
+ this.writers.triggers.trigger(id, length)
1241
+ }
1247
1242
  }
1248
1243
 
1249
1244
  async _applyBatch(batch, optimistic) {
@@ -10,9 +10,12 @@ const DEFAULT_OP_TIMEOUT = 5_000
10
10
  const MIN_FF_GAP = 32
11
11
 
12
12
  class FastForward {
13
- constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
13
+ constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT, conservative = false } = {}) {
14
14
  this.auto = auto
15
15
 
16
+ // conservative: only land when the system and view we booted are held whole
17
+ this.conservative = conservative
18
+
16
19
  this.activeRequests = []
17
20
 
18
21
  // 'reboot' is a storage namespace: renaming it would derive a different
@@ -38,15 +41,11 @@ class FastForward {
38
41
 
39
42
  static async fromHead(auto, head, trusted, { force = false, timeout = 0 } = {}) {
40
43
  const conservative = !force && auto._conservativeFF
41
- const timeoutOpts = timeout ? { timeout } : null
42
44
 
43
- const oplog = await FastForward.flushHead(auto, head, {
44
- conservative,
45
- ...timeoutOpts
46
- })
45
+ const oplog = await FastForward.flushHead(auto, head, { timeout })
47
46
  if (!oplog) return null
48
47
 
49
- const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
48
+ const verified = trusted ? await FastForward.flushHead(auto, trusted, { timeout }) : oplog
50
49
  if (!verified) return null
51
50
 
52
51
  // legacy nodes from non-indexers have no system info to fast-forward from
@@ -56,7 +55,10 @@ class FastForward {
56
55
  return null
57
56
  }
58
57
 
59
- return new FastForward(auto, batchToHead(verified.op.views.system))
58
+ return new FastForward(auto, batchToHead(verified.op.views.system), {
59
+ timeout: timeout || DEFAULT_OP_TIMEOUT,
60
+ conservative
61
+ })
60
62
  }
61
63
 
62
64
  static async fromHeads(auto, heads, { force = false, timeout = 0 } = {}) {
@@ -66,7 +68,7 @@ class FastForward {
66
68
 
67
69
  for (const head of heads) {
68
70
  if (head.length === 0) continue
69
- promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
71
+ promises.push(FastForward.flushHead(auto, head, { timeout }))
70
72
  }
71
73
 
72
74
  const ops = await Promise.all(promises)
@@ -107,7 +109,7 @@ class FastForward {
107
109
  candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
108
110
 
109
111
  for (const res of candidates) {
110
- const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
112
+ const trusted = await FastForward.mostRecentTrusted(auto, res, reference, timeout)
111
113
  if (trusted === null) continue
112
114
  if (auto.fastForwarding || auto.fastForwardTo) return null
113
115
 
@@ -130,8 +132,8 @@ class FastForward {
130
132
  }
131
133
  }
132
134
 
133
- static async mostRecentTrusted(auto, head, reference) {
134
- const opened = auto.openViewAt(head)
135
+ static async mostRecentTrusted(auto, head, reference, timeout) {
136
+ const opened = auto.openViewAt(head, { timeout })
135
137
  if (opened === null) return null
136
138
 
137
139
  try {
@@ -178,8 +180,10 @@ class FastForward {
178
180
  promises.push(this.system.get(this.auto.local.key, { timeout: this.timeout }))
179
181
 
180
182
  // a legacy system without the designated view migrates onto an empty view
183
+ let view = null
184
+
181
185
  if (this.system.view.length) {
182
- const view = this.auto.store.get({ key: this.system.view.key, active: true })
186
+ view = this.auto.store.get({ key: this.system.view.key, active: true })
183
187
  this.cores.push(view)
184
188
 
185
189
  promises.push(
@@ -197,6 +201,13 @@ class FastForward {
197
201
  await Promise.all(promises)
198
202
  if (this.destroyed) return null
199
203
 
204
+ // conservative: the lengths we land on must be held whole, either by us or
205
+ // by a connected peer, so the view does not stall on a block nobody has
206
+ if (this.conservative) {
207
+ if (!isContiguous(this.system.bee.core, this.head.length)) return null
208
+ if (view !== null && !isContiguous(view, this.system.view.length)) return null
209
+ }
210
+
200
211
  return {
201
212
  head: this.head,
202
213
  migrate: null
@@ -206,7 +217,7 @@ class FastForward {
206
217
  _startWarmup() {
207
218
  if (!this.auto._warmup || !this.system.view.length) return false
208
219
 
209
- this.warmupView = this.auto.openView(this.system.view)
220
+ this.warmupView = this.auto.openView(this.system.view, { timeout: this.timeout })
210
221
 
211
222
  // ensure thenable
212
223
  this.warmup = Promise.resolve(this.auto._warmup(this.warmupView.view))
@@ -358,6 +369,10 @@ async function findNewestSystem(ff, info) {
358
369
  return { legacy, autobee }
359
370
  }
360
371
 
372
+ function isContiguous(core, length) {
373
+ return core.contiguousLength >= length || core.remoteContiguousLength >= length
374
+ }
375
+
361
376
  function batchToHead(b) {
362
377
  return {
363
378
  key: b.key,
package/lib/triggers.js CHANGED
@@ -1,9 +1,14 @@
1
+ // writers waiting on a node of another writer, keyed by that writer's id.
2
+ // a writer waits on at most one entry at a time, tracked as writer.waiting
1
3
  module.exports = class Triggers {
2
4
  constructor() {
3
5
  this.waiting = new Map()
4
6
  }
5
7
 
6
8
  add(id, length, writer) {
9
+ // a writer only ever waits on one link: drop whatever it waited on before
10
+ if (writer.waiting !== null && writer.waiting.id !== id) this.remove(writer.waiting)
11
+
7
12
  let w = this.waiting.get(id)
8
13
 
9
14
  if (!w) {
@@ -24,6 +29,7 @@ module.exports = class Triggers {
24
29
  }
25
30
 
26
31
  remove(entry) {
32
+ // writer is nulled on removal, so a removed entry is a noop here
27
33
  if (!entry.writer) return
28
34
 
29
35
  const w = this.waiting.get(entry.id)
@@ -42,14 +48,18 @@ module.exports = class Triggers {
42
48
  const w = this.waiting.get(id)
43
49
  if (!w) return
44
50
 
51
+ // collect first: the walk swap-removes, so nothing may touch the array mid-loop
52
+ const bump = []
53
+
45
54
  for (let i = w.length - 1; i >= 0; i--) {
46
55
  const entry = w[i]
47
56
  if (entry.length > length) continue
48
57
 
49
- const writer = entry.writer
58
+ bump.push(entry.writer)
50
59
  this._remove(w, entry)
51
- writer.bump()
52
60
  }
61
+
62
+ for (const writer of bump) writer.bump()
53
63
  }
54
64
 
55
65
  _remove(w, entry) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -27,7 +27,7 @@
27
27
  "autobee-wakeup": "^1.1.3",
28
28
  "b4a": "^1.8.0",
29
29
  "compact-encoding": "^3.3.0",
30
- "hyperbee2": "^2.10.3",
30
+ "hyperbee2": "^2.16.0",
31
31
  "hypercore": "^11.33.2",
32
32
  "hypercore-id-encoding": "^1.3.0",
33
33
  "hyperschema": "^1.20.1",