autobee 2.0.0-rc.9 → 2.0.0

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.
@@ -0,0 +1,346 @@
1
+ const safetyCatch = require('safety-catch')
2
+ const b4a = require('b4a')
3
+ const Hypercore = require('hypercore')
4
+
5
+ const System = require('./system.js')
6
+ const migrations = require('./migrations.js')
7
+ const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
8
+
9
+ const DEFAULT_OP_TIMEOUT = 5_000
10
+ const MIN_FF_GAP = 32
11
+
12
+ module.exports = class FastForward {
13
+ constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
14
+ this.auto = auto
15
+
16
+ this.activeRequests = []
17
+
18
+ // 'reboot' is a storage namespace: renaming it would derive a different
19
+ // scratch core on disk, so it stays as is
20
+ this.system = new System(auto.store.namespace('reboot'), null, {
21
+ getEncryptionProvider: this.auto.getSystemEncryption,
22
+ encrypted: this.encrypted,
23
+ activeRequests: this.activeRequests
24
+ })
25
+
26
+ // length -1 marks an uninitialised head: we know the key but not the length
27
+ this.head = { key: head.key, length: head.length ?? 0 }
28
+ this.timeout = timeout
29
+ this.destroyed = false
30
+ this.running = null
31
+ this.failed = false
32
+ this.cores = []
33
+ }
34
+
35
+ static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
36
+
37
+ static async fromHead(
38
+ auto,
39
+ head,
40
+ trusted,
41
+ { force = false, timeout = 0, condition = null, reference = null } = {}
42
+ ) {
43
+ const conservative = !force && auto._conservativeFF
44
+ const timeoutOpts = timeout ? { timeout } : null
45
+
46
+ const oplog = await FastForward.flushHead(auto, head, {
47
+ conservative,
48
+ ...timeoutOpts
49
+ })
50
+ if (!oplog) return null
51
+
52
+ const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
53
+ if (!verified) return null
54
+
55
+ // legacy nodes from non-indexers have no system info to fast-forward from
56
+ if (!oplog.op.views || !verified.op.views) return null
57
+
58
+ if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
59
+ return null
60
+ }
61
+
62
+ if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
63
+ return null
64
+ }
65
+
66
+ return new FastForward(auto, batchToHead(verified.op.views.system))
67
+ }
68
+
69
+ static async fromHeads(
70
+ auto,
71
+ heads,
72
+ { force = false, timeout = 0, condition = null, reference = null } = {}
73
+ ) {
74
+ if (reference === null) reference = auto._workingView.view
75
+
76
+ const promises = []
77
+
78
+ for (const head of heads) {
79
+ if (head.length === 0) continue
80
+ promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
81
+ }
82
+
83
+ const ops = await Promise.all(promises)
84
+ if (auto.fastForwarding || auto.fastForwardTo) return null
85
+
86
+ const trust = await Promise.all(
87
+ ops.map((res) => {
88
+ return res === null ? false : auto.trusted.isTrusted(res.key, reference)
89
+ })
90
+ )
91
+ if (auto.fastForwarding || auto.fastForwardTo) return null
92
+
93
+ const candidates = []
94
+
95
+ let bestTrusted = null
96
+ let bestTrustedFlushes = -1
97
+
98
+ for (let i = 0; i < ops.length; i++) {
99
+ const res = ops[i]
100
+ if (res === null || !res.op.views) continue
101
+ if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
102
+
103
+ if (!trust[i]) {
104
+ candidates.push(res)
105
+ continue
106
+ }
107
+
108
+ if (res.op.views.flushes > bestTrustedFlushes) {
109
+ bestTrustedFlushes = res.op.views.flushes
110
+ bestTrusted = res
111
+ }
112
+ }
113
+
114
+ if (bestTrusted !== null) {
115
+ return FastForward.fromHead(auto, bestTrusted, null, {
116
+ force,
117
+ timeout,
118
+ condition,
119
+ reference
120
+ })
121
+ }
122
+
123
+ candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
124
+
125
+ for (const res of candidates) {
126
+ const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
127
+ if (trusted === null) continue
128
+ if (auto.fastForwarding || auto.fastForwardTo) return null
129
+
130
+ const ff = await FastForward.fromHead(auto, res, trusted, {
131
+ force,
132
+ timeout,
133
+ condition,
134
+ reference
135
+ })
136
+ if (ff !== null) return ff
137
+ }
138
+
139
+ return null
140
+ }
141
+
142
+ static async flushHead(auto, head, opts) {
143
+ const core = auto.openCore(head.key)
144
+
145
+ try {
146
+ const oplog = await auto.readOplog(core, head.length, opts)
147
+ if (oplog === null) return null
148
+ return oplog.op.views ? oplog : null
149
+ } finally {
150
+ await core.close()
151
+ }
152
+ }
153
+
154
+ static async accepts(auto, oplog, condition, reference) {
155
+ const opened = auto.openViewAt(oplog)
156
+ if (opened === null) return false
157
+
158
+ try {
159
+ return !!(await condition(opened.view, reference))
160
+ } finally {
161
+ await opened.close()
162
+ }
163
+ }
164
+
165
+ static async mostRecentTrusted(auto, head, reference) {
166
+ const opened = auto.openViewAt(head)
167
+ if (opened === null) return null
168
+
169
+ try {
170
+ return (await auto.trusted.mostRecentTrusted(opened.view, reference)) || null
171
+ } finally {
172
+ await opened.close()
173
+ }
174
+ }
175
+
176
+ async run() {
177
+ try {
178
+ if (!this.running) this.running = this._run()
179
+
180
+ return await this.running
181
+ } catch (err) {
182
+ safetyCatch(err)
183
+ this.failed = true
184
+ return null
185
+ } finally {
186
+ await this.close()
187
+ }
188
+ }
189
+
190
+ async _run() {
191
+ // an uninitialised head has an unknown length: wait for the core to catch up
192
+ if (!this.head.length) {
193
+ this.head.length = await this._resolveLength()
194
+ }
195
+
196
+ // zero-length boot is unsupported
197
+ if (!this.head.length) return null
198
+
199
+ await this.system.boot(this.head, { timeout: this.timeout })
200
+
201
+ // a legacy system needs migrating rather than a plain fast-forward
202
+ if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
203
+ const migrated = this.auto._migratedHead && this.auto._migratedHead.system
204
+ if (
205
+ migrated &&
206
+ b4a.equals(migrated.key, this.head.key) &&
207
+ migrated.length >= this.head.length
208
+ ) {
209
+ return null
210
+ }
211
+
212
+ const result = await this._migrate()
213
+ if (result === null || result.autobee === null) return result
214
+
215
+ // an indexer of the newest legacy generation already moved to autobee -
216
+ // fast-forward straight onto its announced head instead of migrating,
217
+ // falling back to the legacy migration if the head cannot be booted
218
+ try {
219
+ this.head = result.autobee
220
+ await this.system.boot(this.head, { timeout: this.timeout })
221
+ if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
222
+ throw new Error('Expected an autobee system')
223
+ }
224
+ } catch (err) {
225
+ safetyCatch(err)
226
+ this.head = result.head
227
+ return { head: result.head, migrate: result.migrate }
228
+ }
229
+
230
+ // fall through to the plain fast-forward below
231
+ }
232
+
233
+ const promises = []
234
+
235
+ // ensure local key is locally available always
236
+ promises.push(this.system.get(this.auto.local.key, { timeout: this.timeout }))
237
+
238
+ const view = this.auto.store.get({ key: this.system.view.key, active: true })
239
+ this.cores.push(view)
240
+
241
+ promises.push(
242
+ view.get(this.system.view.length - 1, {
243
+ timeout: this.timeout,
244
+ activeRequests: this.activeRequests
245
+ })
246
+ )
247
+
248
+ for (const head of this.system.heads) {
249
+ promises.push(this.system.get(head.key, { timeout: this.timeout }))
250
+ }
251
+
252
+ await Promise.all(promises)
253
+ if (this.destroyed) return null
254
+
255
+ if (this.destroyed) return null
256
+
257
+ return {
258
+ head: this.head,
259
+ migrate: null
260
+ }
261
+ }
262
+
263
+ // Upgrade a legacy (pre-AUTOBEE_VERSION) system in place. The system already
264
+ // booted above (coerced to the current struct), but the original v1/v2 struct
265
+ // is needed to recover the indexers/views/entropy, so decode the raw block.
266
+ //
267
+ // A head may be arbitrarily old: an indexer rotation freezes the system core
268
+ // and moves the indexers to a successor. The frozen core cannot reveal its
269
+ // successor (the promotion only lands in its unsigned tail), but the
270
+ // indexers' newest oplog digests stamp the newest system they know - chase
271
+ // those so the migration lands on the newest generation instead of the head
272
+ // we were given.
273
+ async _migrate() {
274
+ let head = { key: this.head.key, length: this.head.length }
275
+ const seen = new Set()
276
+
277
+ for (let hops = 0; hops < migrations.MAX_MIGRATE_HOPS; hops++) {
278
+ seen.add(b4a.toString(head.key, 'hex'))
279
+
280
+ const core = this.auto.store.get({ key: head.key, encryption: null })
281
+ this.cores.push(core)
282
+ await core.ready()
283
+
284
+ // setup encryption
285
+ await core.setEncryption(this.auto.getSystemEncryption())
286
+
287
+ // successors are resolved by key only - wait for the replicated length
288
+ if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
289
+ if (!head.length || this.destroyed) return null
290
+
291
+ const info = await migrations.readLegacySystemInfo(core, head.length, {
292
+ timeout: this.timeout,
293
+ activeRequests: this.activeRequests
294
+ })
295
+
296
+ const { legacy, autobee } = await migrations.findNewestSystem(this, info)
297
+ if (this.destroyed) return null
298
+
299
+ if (legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
300
+ head = { key: legacy, length: 0 }
301
+ continue
302
+ }
303
+
304
+ this.head = head
305
+
306
+ const views = await migrations.resolveLegacyViews(this, info)
307
+ if (views === null || this.destroyed) return null
308
+
309
+ return {
310
+ head,
311
+ migrate: views,
312
+ autobee: autobee === null ? null : batchToHead(autobee.op.views.system)
313
+ }
314
+ }
315
+
316
+ return null
317
+ }
318
+
319
+ async _resolveLength() {
320
+ const core = this.auto.store.get({ key: this.head.key })
321
+ this.cores.push(core)
322
+
323
+ await core.ready()
324
+
325
+ return migrations.coreLength(core, this.timeout)
326
+ }
327
+
328
+ // cancels every read in flight and fails any that come after
329
+ clearRequests(err = null) {
330
+ Hypercore.destroyRequests(this.activeRequests, err)
331
+ }
332
+
333
+ async close() {
334
+ this.destroyed = true
335
+ this.clearRequests()
336
+ if (this.system) await this.system.close()
337
+ for (const core of this.cores) await core.close()
338
+ }
339
+ }
340
+
341
+ function batchToHead(b) {
342
+ return {
343
+ key: b.key,
344
+ length: b.start + b.length
345
+ }
346
+ }