autobee 2.2.1 → 2.3.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.
- package/README.md +8 -7
- package/index.js +58 -173
- package/lib/boot.js +4 -10
- package/lib/encoding.js +1 -12
- package/lib/fast-forward.js +6 -132
- package/lib/migrations.js +44 -197
- package/lib/system.js +41 -21
- package/lib/topo.js +5 -19
- package/lib/updates.js +2 -1
- package/lib/writers.js +73 -42
- package/package.json +2 -1
package/lib/fast-forward.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
const safetyCatch = require('safety-catch')
|
|
2
|
-
const b4a = require('b4a')
|
|
3
2
|
const Hypercore = require('hypercore')
|
|
4
3
|
|
|
5
4
|
const System = require('./system.js')
|
|
6
5
|
const migrations = require('./migrations.js')
|
|
7
|
-
const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
|
|
8
6
|
|
|
9
7
|
const DEFAULT_OP_TIMEOUT = 5_000
|
|
10
8
|
const MIN_FF_GAP = 32
|
|
@@ -17,7 +15,7 @@ module.exports = class FastForward {
|
|
|
17
15
|
|
|
18
16
|
// 'reboot' is a storage namespace: renaming it would derive a different
|
|
19
17
|
// scratch core on disk, so it stays as is
|
|
20
|
-
this.system = new System(auto.store.namespace('reboot'),
|
|
18
|
+
this.system = new System(auto, auto.store.namespace('reboot'), {
|
|
21
19
|
getEncryptionProvider: this.auto.getSystemEncryption,
|
|
22
20
|
encrypted: this.encrypted,
|
|
23
21
|
activeRequests: this.activeRequests
|
|
@@ -34,12 +32,7 @@ module.exports = class FastForward {
|
|
|
34
32
|
|
|
35
33
|
static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
|
|
36
34
|
|
|
37
|
-
static async fromHead(
|
|
38
|
-
auto,
|
|
39
|
-
head,
|
|
40
|
-
trusted,
|
|
41
|
-
{ force = false, timeout = 0, condition = null, reference = null } = {}
|
|
42
|
-
) {
|
|
35
|
+
static async fromHead(auto, head, trusted, { force = false, timeout = 0 } = {}) {
|
|
43
36
|
const conservative = !force && auto._conservativeFF
|
|
44
37
|
const timeoutOpts = timeout ? { timeout } : null
|
|
45
38
|
|
|
@@ -59,19 +52,11 @@ module.exports = class FastForward {
|
|
|
59
52
|
return null
|
|
60
53
|
}
|
|
61
54
|
|
|
62
|
-
if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
55
|
return new FastForward(auto, batchToHead(verified.op.views.system))
|
|
67
56
|
}
|
|
68
57
|
|
|
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
|
|
58
|
+
static async fromHeads(auto, heads, { force = false, timeout = 0 } = {}) {
|
|
59
|
+
const reference = auto._workingView.view
|
|
75
60
|
|
|
76
61
|
const promises = []
|
|
77
62
|
|
|
@@ -112,12 +97,7 @@ module.exports = class FastForward {
|
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
if (bestTrusted !== null) {
|
|
115
|
-
return FastForward.fromHead(auto, bestTrusted, null, {
|
|
116
|
-
force,
|
|
117
|
-
timeout,
|
|
118
|
-
condition,
|
|
119
|
-
reference
|
|
120
|
-
})
|
|
100
|
+
return FastForward.fromHead(auto, bestTrusted, null, { force, timeout })
|
|
121
101
|
}
|
|
122
102
|
|
|
123
103
|
candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
|
|
@@ -127,12 +107,7 @@ module.exports = class FastForward {
|
|
|
127
107
|
if (trusted === null) continue
|
|
128
108
|
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
129
109
|
|
|
130
|
-
const ff = await FastForward.fromHead(auto, res, trusted, {
|
|
131
|
-
force,
|
|
132
|
-
timeout,
|
|
133
|
-
condition,
|
|
134
|
-
reference
|
|
135
|
-
})
|
|
110
|
+
const ff = await FastForward.fromHead(auto, res, trusted, { force, timeout })
|
|
136
111
|
if (ff !== null) return ff
|
|
137
112
|
}
|
|
138
113
|
|
|
@@ -151,17 +126,6 @@ module.exports = class FastForward {
|
|
|
151
126
|
}
|
|
152
127
|
}
|
|
153
128
|
|
|
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
129
|
static async mostRecentTrusted(auto, head, reference) {
|
|
166
130
|
const opened = auto.openViewAt(head)
|
|
167
131
|
if (opened === null) return null
|
|
@@ -198,38 +162,6 @@ module.exports = class FastForward {
|
|
|
198
162
|
|
|
199
163
|
await this.system.boot(this.head, { timeout: this.timeout })
|
|
200
164
|
|
|
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
165
|
const promises = []
|
|
234
166
|
|
|
235
167
|
// ensure local key is locally available always
|
|
@@ -252,70 +184,12 @@ module.exports = class FastForward {
|
|
|
252
184
|
await Promise.all(promises)
|
|
253
185
|
if (this.destroyed) return null
|
|
254
186
|
|
|
255
|
-
if (this.destroyed) return null
|
|
256
|
-
|
|
257
187
|
return {
|
|
258
188
|
head: this.head,
|
|
259
189
|
migrate: null
|
|
260
190
|
}
|
|
261
191
|
}
|
|
262
192
|
|
|
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
193
|
async _resolveLength() {
|
|
320
194
|
const core = this.auto.store.get({ key: this.head.key })
|
|
321
195
|
this.cores.push(core)
|
package/lib/migrations.js
CHANGED
|
@@ -6,7 +6,6 @@ const { decodeBlock } = require('hyperbee2/lib/encoding.js')
|
|
|
6
6
|
|
|
7
7
|
const encoding = require('./encoding.js')
|
|
8
8
|
const { assert, bail } = require('./asserts.js')
|
|
9
|
-
const { LEGACY_OPLOG_VERSION } = require('./constants.js')
|
|
10
9
|
|
|
11
10
|
const { getEncoding } = require('../encoding/spec/autobee')
|
|
12
11
|
|
|
@@ -18,23 +17,52 @@ const INDEX_VERSION = 1
|
|
|
18
17
|
const INFO = b4a.from([0x00, 0x00, 0x69, 0x6e, 0x66, 0x6f])
|
|
19
18
|
const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
20
19
|
|
|
21
|
-
// indexer rotations chain legacy systems - how many generations we chase
|
|
22
|
-
const MAX_MIGRATE_HOPS = 64
|
|
23
|
-
|
|
24
20
|
module.exports = {
|
|
25
|
-
|
|
21
|
+
getCoreManifest,
|
|
22
|
+
getViewMap,
|
|
26
23
|
checkAutobaseMigration,
|
|
27
|
-
readLegacySystemInfo,
|
|
28
|
-
findNewestSystem,
|
|
29
|
-
resolveLegacyViews,
|
|
30
24
|
inflateLegacyOplog,
|
|
31
|
-
decodeLegacySystemInfo,
|
|
32
|
-
deriveNamespace,
|
|
33
25
|
coreLength
|
|
34
26
|
}
|
|
35
27
|
|
|
28
|
+
async function getCoreManifest(store, key, { timeout, wait = true } = {}) {
|
|
29
|
+
const core = store.get(key)
|
|
30
|
+
await core.ready()
|
|
31
|
+
try {
|
|
32
|
+
if (!core.manifest && core.length === 0) await core.get(0, { timeout, wait })
|
|
33
|
+
return core.manifest
|
|
34
|
+
} finally {
|
|
35
|
+
await core.close()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// local-only probes: the manifests are expected in storage already (prefetched
|
|
40
|
+
// by the reset path), so a violation skips instead of hanging on the network
|
|
41
|
+
async function getViewMap(store, bootstrap, encryptionKey, info, legacyViews) {
|
|
42
|
+
const getIndexerManifest = ({ key }) => getCoreManifest(store, key, { wait: false })
|
|
43
|
+
const indexerManifests = await Promise.all(info.indexers.map(getIndexerManifest))
|
|
44
|
+
const entropy = legacyEntropy(info, indexerManifests)
|
|
45
|
+
const mappedViews = new Map()
|
|
46
|
+
|
|
47
|
+
for (const name of legacyViews) {
|
|
48
|
+
const v = await findViewByName(
|
|
49
|
+
store,
|
|
50
|
+
bootstrap.key,
|
|
51
|
+
encryptionKey,
|
|
52
|
+
indexerManifests,
|
|
53
|
+
info.views,
|
|
54
|
+
entropy,
|
|
55
|
+
name
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
mappedViews.set(name, v)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return mappedViews
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
// boot-time migration of local legacy autobase storage
|
|
37
|
-
async function checkAutobaseMigration(store, local, bootstrap
|
|
65
|
+
async function checkAutobaseMigration(store, local, bootstrap) {
|
|
38
66
|
const bootRecord = await local.getUserData('autobase/boot')
|
|
39
67
|
if (!bootRecord) return null
|
|
40
68
|
|
|
@@ -60,41 +88,19 @@ async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
|
|
|
60
88
|
await session.close()
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
|
|
91
|
+
// nothing reads the info here anymore, but a legacy system that will not
|
|
92
|
+
// decode has to throw for migrateWithFallback pathway
|
|
93
|
+
await readLegacySystemInfo(core, length, { wait: false })
|
|
64
94
|
|
|
65
95
|
const nodes = await Promise.all(
|
|
66
96
|
catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
|
|
67
97
|
)
|
|
68
98
|
|
|
69
|
-
const indexerManifests = await Promise.all(
|
|
70
|
-
info.indexers.map((idx) => storeCoreManifest(store, idx.key))
|
|
71
|
-
)
|
|
72
|
-
const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
|
|
73
|
-
const entropy = legacyEntropy(info, indexerManifests)
|
|
74
|
-
|
|
75
|
-
const getManifest = (key) => storeCoreManifest(store, key)
|
|
76
|
-
|
|
77
|
-
const mappedViews = new Map()
|
|
78
|
-
for (const name of legacyViews) {
|
|
79
|
-
const v = await findViewByName(
|
|
80
|
-
getManifest,
|
|
81
|
-
bootstrap.key,
|
|
82
|
-
encryptionKey,
|
|
83
|
-
indexerManifests,
|
|
84
|
-
views,
|
|
85
|
-
entropy,
|
|
86
|
-
name
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
mappedViews.set(name, v)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
99
|
await core.close()
|
|
93
100
|
|
|
94
101
|
return {
|
|
95
102
|
encryptionKey,
|
|
96
103
|
system,
|
|
97
|
-
views: mappedViews,
|
|
98
104
|
catchup: nodes
|
|
99
105
|
}
|
|
100
106
|
}
|
|
@@ -112,95 +118,6 @@ async function readLegacySystemInfo(core, length, opts = null) {
|
|
|
112
118
|
return decodeLegacySystemInfo(node.keys[0].value)
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
// read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
|
|
116
|
-
// the newest legacy system that indexer knows. take the majority vote. an
|
|
117
|
-
// indexer that already moved to autobee announces its live autobee state
|
|
118
|
-
// instead - keep the newest of those as a direct fast-forward candidate and
|
|
119
|
-
// bisect down to its buried legacy stamps so its vote still counts
|
|
120
|
-
async function findNewestSystem(ff, info) {
|
|
121
|
-
const tally = new Map()
|
|
122
|
-
|
|
123
|
-
let autobee = null
|
|
124
|
-
|
|
125
|
-
for (const idx of info.indexers) {
|
|
126
|
-
const core = ff.auto.openCore(idx.key)
|
|
127
|
-
ff.cores.push(core)
|
|
128
|
-
await core.ready()
|
|
129
|
-
|
|
130
|
-
const length = await coreLength(core, ff.timeout)
|
|
131
|
-
if (!length || ff.destroyed) continue
|
|
132
|
-
|
|
133
|
-
let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
|
|
134
|
-
if (oplog === null || !oplog.op.views) continue
|
|
135
|
-
|
|
136
|
-
if (oplog.op.version > LEGACY_OPLOG_VERSION) {
|
|
137
|
-
if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
|
|
138
|
-
autobee = oplog
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
|
|
142
|
-
if (!boundary || ff.destroyed) continue
|
|
143
|
-
|
|
144
|
-
oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
|
|
145
|
-
if (oplog === null || !oplog.op.views) continue
|
|
146
|
-
if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const sys = oplog.op.views.system
|
|
150
|
-
if (!sys || !sys.key) continue
|
|
151
|
-
|
|
152
|
-
const hex = b4a.toString(sys.key, 'hex')
|
|
153
|
-
const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
|
|
154
|
-
|
|
155
|
-
t.votes++
|
|
156
|
-
if (sys.length > t.length) t.length = sys.length
|
|
157
|
-
tally.set(hex, t)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const majority = (info.indexers.length >> 1) + 1
|
|
161
|
-
|
|
162
|
-
let best = null
|
|
163
|
-
for (const t of tally.values()) {
|
|
164
|
-
if (t.votes >= majority && (best === null || t.length > best.length)) best = t
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { legacy: best === null ? null : best.key, autobee }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// map the legacy views recorded in the system info by name
|
|
171
|
-
async function resolveLegacyViews(ff, info) {
|
|
172
|
-
const indexerManifests = await Promise.all(
|
|
173
|
-
info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
|
|
174
|
-
)
|
|
175
|
-
if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
|
|
176
|
-
|
|
177
|
-
const entropy = legacyEntropy(info, indexerManifests)
|
|
178
|
-
|
|
179
|
-
const getManifest = (key, length) => ffCoreManifest(ff, key, length)
|
|
180
|
-
|
|
181
|
-
const views = new Map()
|
|
182
|
-
|
|
183
|
-
await Promise.all(
|
|
184
|
-
ff.auto.legacyViews.map(async (name) => {
|
|
185
|
-
const v = await findViewByName(
|
|
186
|
-
getManifest,
|
|
187
|
-
ff.auto.key,
|
|
188
|
-
ff.auto.encryptionKey,
|
|
189
|
-
indexerManifests,
|
|
190
|
-
info.views,
|
|
191
|
-
entropy,
|
|
192
|
-
name
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
if (v) views.set(name, v)
|
|
196
|
-
})
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
if (ff.destroyed) return null
|
|
200
|
-
|
|
201
|
-
return views
|
|
202
|
-
}
|
|
203
|
-
|
|
204
121
|
// legacy nodes always inflate, but only indexers carry the digest/checkpoint
|
|
205
122
|
// stamps that synthesise system info - callers must check op.views
|
|
206
123
|
async function inflateLegacyOplog(buf, core, seq, timeout) {
|
|
@@ -309,32 +226,12 @@ function coreLength(core, timeout) {
|
|
|
309
226
|
})
|
|
310
227
|
}
|
|
311
228
|
|
|
312
|
-
// a writer's oplog is a legacy prefix followed by autobee-format nodes -
|
|
313
|
-
// bisect for the boundary. lo is a length known to be legacy (the legacy
|
|
314
|
-
// system's recorded length for this writer), hi a seq known to be autobee.
|
|
315
|
-
// returns the legacy prefix length
|
|
316
|
-
async function findLegacyBoundary(ff, core, lo, hi) {
|
|
317
|
-
while (lo < hi) {
|
|
318
|
-
const mid = (lo + hi) >>> 1
|
|
319
|
-
|
|
320
|
-
const block = await core.get(mid, {
|
|
321
|
-
timeout: ff.timeout,
|
|
322
|
-
activeRequests: ff.activeRequests
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
|
|
326
|
-
else lo = mid + 1
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return lo
|
|
330
|
-
}
|
|
331
|
-
|
|
332
229
|
function legacyEntropy(info, indexerManifests) {
|
|
333
230
|
return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
|
|
334
231
|
}
|
|
335
232
|
|
|
336
233
|
async function findViewByName(
|
|
337
|
-
|
|
234
|
+
store,
|
|
338
235
|
bootstrap,
|
|
339
236
|
encryptionKey,
|
|
340
237
|
indexerManifests,
|
|
@@ -347,7 +244,7 @@ async function findViewByName(
|
|
|
347
244
|
const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
|
|
348
245
|
|
|
349
246
|
for (const v of views) {
|
|
350
|
-
const manifest = await
|
|
247
|
+
const manifest = await getCoreManifest(store, v.key, { wait: false })
|
|
351
248
|
if (!manifest) continue
|
|
352
249
|
|
|
353
250
|
if (manifest.signers.length === 0) continue
|
|
@@ -360,50 +257,6 @@ async function findViewByName(
|
|
|
360
257
|
return null
|
|
361
258
|
}
|
|
362
259
|
|
|
363
|
-
// request-tracked manifest read over replication
|
|
364
|
-
async function ffCoreManifest(ff, key, length) {
|
|
365
|
-
const core = ff.auto.store.get(key)
|
|
366
|
-
ff.cores.push(core)
|
|
367
|
-
|
|
368
|
-
await core.ready()
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
if (!core.manifest) {
|
|
372
|
-
await core.get(length - 1, {
|
|
373
|
-
timeout: ff.timeout,
|
|
374
|
-
activeRequests: ff.activeRequests
|
|
375
|
-
})
|
|
376
|
-
}
|
|
377
|
-
} catch {
|
|
378
|
-
return null
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return core.manifest
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// local-storage manifest read
|
|
385
|
-
async function storeCoreManifest(store, key) {
|
|
386
|
-
const core = store.get(key)
|
|
387
|
-
await core.ready()
|
|
388
|
-
|
|
389
|
-
const manifest = core.manifest
|
|
390
|
-
await core.close()
|
|
391
|
-
|
|
392
|
-
return manifest
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
async function getPersistedView(store, view) {
|
|
396
|
-
const core = store.get(view.key)
|
|
397
|
-
await core.ready()
|
|
398
|
-
|
|
399
|
-
// core.length can run past what was downloaded - the system's recorded
|
|
400
|
-
// length is the last one we know is local
|
|
401
|
-
const length = Math.min(core.length, view.length)
|
|
402
|
-
await core.close()
|
|
403
|
-
|
|
404
|
-
return { key: view.key, length }
|
|
405
|
-
}
|
|
406
|
-
|
|
407
260
|
function getOplog(info, batch) {
|
|
408
261
|
if (batch.length === 1) return batch[0].keys[0]
|
|
409
262
|
|
|
@@ -489,9 +342,3 @@ async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
|
|
|
489
342
|
|
|
490
343
|
return batch
|
|
491
344
|
}
|
|
492
|
-
|
|
493
|
-
// lazy to avoid the require cycle - fast-forward imports this module
|
|
494
|
-
function flushHead(auto, head, opts) {
|
|
495
|
-
const FastForward = require('./fast-forward.js')
|
|
496
|
-
return FastForward.flushHead(auto, head, opts)
|
|
497
|
-
}
|
package/lib/system.js
CHANGED
|
@@ -2,7 +2,9 @@ const Hyperbee = require('hyperbee2')
|
|
|
2
2
|
const Promotions = require('./promotions.js')
|
|
3
3
|
const b4a = require('b4a')
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { getViewMap, getCoreManifest } = require('./migrations.js')
|
|
6
|
+
|
|
7
|
+
const { AUTOBEE_VERSION, LEGACY_AUTOBASE_VERSION } = require('./constants')
|
|
6
8
|
const encoding = require('./encoding.js')
|
|
7
9
|
const topo = require('./topo.js')
|
|
8
10
|
|
|
@@ -33,12 +35,13 @@ class SystemSnapshot {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
module.exports = class Systembee {
|
|
36
|
-
constructor(
|
|
37
|
-
this.
|
|
38
|
+
constructor(auto, store, opts = {}) {
|
|
39
|
+
this.auto = auto
|
|
38
40
|
this.store = store
|
|
39
41
|
this.bee = new Hyperbee(store, opts)
|
|
40
42
|
this.view = null
|
|
41
43
|
this.heads = []
|
|
44
|
+
this.migration = null
|
|
42
45
|
this.version = 0
|
|
43
46
|
this.timestamp = 0
|
|
44
47
|
this.flushes = 0
|
|
@@ -48,6 +51,10 @@ module.exports = class Systembee {
|
|
|
48
51
|
this.promotions = new Promotions(this.bee)
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
get name() {
|
|
55
|
+
return this.auto.name
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
async addWriter(
|
|
52
59
|
key,
|
|
53
60
|
{ length = 0, weight = 1, isGenesis = false, coord = null, carrier = -1, anchor = false } = {}
|
|
@@ -127,9 +134,17 @@ module.exports = class Systembee {
|
|
|
127
134
|
|
|
128
135
|
this.version = info ? info.version : 0
|
|
129
136
|
this.view = info ? info.view : EMPTY_HEAD
|
|
137
|
+
this.migration = null
|
|
130
138
|
this.heads = info ? info.heads : []
|
|
131
139
|
this.timestamp = info ? info.timestamp : 0
|
|
132
140
|
this.flushes = info ? info.flushes : 0
|
|
141
|
+
|
|
142
|
+
if (this.version <= LEGACY_AUTOBASE_VERSION) {
|
|
143
|
+
// ~~~ good enough estimation
|
|
144
|
+
this.flushes = Math.floor(this.bee.core.length / 2)
|
|
145
|
+
this.view = await this._migrate(info, { timeout })
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
this.updates.clear()
|
|
134
149
|
this.writers.clear()
|
|
135
150
|
this.promotions.reset(info)
|
|
@@ -139,6 +154,29 @@ module.exports = class Systembee {
|
|
|
139
154
|
}
|
|
140
155
|
}
|
|
141
156
|
|
|
157
|
+
async _migrate(info, { timeout } = {}) {
|
|
158
|
+
if (!info) return EMPTY_HEAD
|
|
159
|
+
const a = this.auto
|
|
160
|
+
|
|
161
|
+
const toManifests = ({ key }) => getCoreManifest(a.store, key, { timeout })
|
|
162
|
+
const promises = [...info.indexers.map(toManifests), ...info.views.map(toManifests)]
|
|
163
|
+
|
|
164
|
+
await Promise.all(promises)
|
|
165
|
+
|
|
166
|
+
const views = await getViewMap(a.store, a.bootstrap, a.encryptionKey, info, a.legacyViews)
|
|
167
|
+
|
|
168
|
+
// the handler only runs once the head is locked in - see auto._runMigration
|
|
169
|
+
this.migration = { views, head: this.bee.head() }
|
|
170
|
+
|
|
171
|
+
// legacyViews is ordered by preference, so the first one that resolved wins
|
|
172
|
+
for (const name of a.legacyViews) {
|
|
173
|
+
const view = views.get(name)
|
|
174
|
+
if (view) return view
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return EMPTY_HEAD
|
|
178
|
+
}
|
|
179
|
+
|
|
142
180
|
bootRecord() {
|
|
143
181
|
const system = this.bee.head()
|
|
144
182
|
return system.length ? system : null
|
|
@@ -441,21 +479,3 @@ module.exports = class Systembee {
|
|
|
441
479
|
return this.promotions.hint(key, opts)
|
|
442
480
|
}
|
|
443
481
|
}
|
|
444
|
-
|
|
445
|
-
function getInfo(data) {
|
|
446
|
-
for (const { keys } of data.batch) {
|
|
447
|
-
for (const k of keys) {
|
|
448
|
-
const prefix = k.key[0]
|
|
449
|
-
|
|
450
|
-
if (prefix === 0) {
|
|
451
|
-
try {
|
|
452
|
-
return encoding.decodeSystemInfo(k.value)
|
|
453
|
-
} catch {
|
|
454
|
-
return null
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return null
|
|
461
|
-
}
|
package/lib/topo.js
CHANGED
|
@@ -7,15 +7,11 @@ const { LEGACY_OPLOG_VERSION } = require('./constants.js')
|
|
|
7
7
|
const EMPTY_HEAD = { length: 0, key: null }
|
|
8
8
|
const INFO_LEGACY_KEY = b4a.concat([b4a.from([0, 0]), b4a.from('info')])
|
|
9
9
|
|
|
10
|
-
const DEFAULT_OP_TIMEOUT = 5000
|
|
11
|
-
|
|
12
10
|
exports.sort = sort
|
|
13
11
|
exports.replay = replay
|
|
14
|
-
exports.getMostRecentHead = getMostRecentHead
|
|
15
12
|
|
|
16
13
|
exports.isLinking = isLinking
|
|
17
14
|
exports.isLinkingAll = isLinkingAll
|
|
18
|
-
exports.isLegacyNode = isLegacyNode
|
|
19
15
|
|
|
20
16
|
class Clock {
|
|
21
17
|
constructor() {
|
|
@@ -35,19 +31,6 @@ class Clock {
|
|
|
35
31
|
}
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
async function getMostRecentHead(auto, system) {
|
|
39
|
-
const ch = system.createChangesStream()
|
|
40
|
-
for await (const data of ch) {
|
|
41
|
-
const refs = getOplogCompat(data)
|
|
42
|
-
if (!refs.length) continue
|
|
43
|
-
|
|
44
|
-
const { oplog } = refs[0]
|
|
45
|
-
return { key: oplog.key, length: oplog.length }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
34
|
// nodes we cannot resolve are returned as null rather than dropped, so callers
|
|
52
35
|
// can tell an incomplete replay from a short one
|
|
53
36
|
async function replay(auto) {
|
|
@@ -181,7 +164,7 @@ function cmp(a, b) {
|
|
|
181
164
|
return a.length < b.length ? -1 : 1
|
|
182
165
|
}
|
|
183
166
|
|
|
184
|
-
async function inflate(auto, reorder, { wait = true, timeout =
|
|
167
|
+
async function inflate(auto, reorder, { wait = true, timeout = 0 } = {}) {
|
|
185
168
|
const readOpts = wait ? { timeout } : { wait: false }
|
|
186
169
|
|
|
187
170
|
const promises = []
|
|
@@ -233,7 +216,7 @@ async function getOplogBatch(undo, core, length, weight, index, readOpts = null)
|
|
|
233
216
|
const legacy = oplog.version <= LEGACY_OPLOG_VERSION
|
|
234
217
|
|
|
235
218
|
// if this is legacy the start pointer is always 0, fix
|
|
236
|
-
if (legacy) {
|
|
219
|
+
if (legacy && !oplog.links.length) {
|
|
237
220
|
for (let i = length - 1; i > 0; i--) {
|
|
238
221
|
const block = await core.get(i - 1, readOpts)
|
|
239
222
|
if (block === null) return null // wait: false in replay helper
|
|
@@ -241,6 +224,7 @@ async function getOplogBatch(undo, core, length, weight, index, readOpts = null)
|
|
|
241
224
|
const oplog = encoding.decodeOplog(block)
|
|
242
225
|
if (oplog.batch.end === 0) break
|
|
243
226
|
head.batch.start++
|
|
227
|
+
if (oplog.links.length > 0) break // always start of batch in legacy
|
|
244
228
|
}
|
|
245
229
|
}
|
|
246
230
|
|
|
@@ -345,6 +329,8 @@ function getOplogCompat(data) {
|
|
|
345
329
|
const candidates = []
|
|
346
330
|
|
|
347
331
|
for (const { keys } of data.batch) {
|
|
332
|
+
if (!keys) continue
|
|
333
|
+
|
|
348
334
|
for (const k of keys) {
|
|
349
335
|
if (k.key[0] !== 1) continue
|
|
350
336
|
|