autobee 2.0.0-rc.8 → 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.
- package/README.md +48 -1
- package/encoding/compat.js +1 -1
- package/encoding/legacy.js +33 -8
- package/encoding/spec/autobee/index.js +312 -25
- package/encoding/spec/autobee/schema.json +228 -2
- package/index.js +555 -310
- package/lib/apply-calls.js +5 -6
- package/lib/apply-view.js +25 -0
- package/lib/boot.js +14 -182
- package/lib/constants.js +3 -6
- package/lib/encoding.js +72 -3
- package/lib/fast-forward.js +346 -0
- package/lib/migrations.js +457 -0
- package/lib/promotions.js +164 -0
- package/lib/system.js +97 -179
- package/lib/topo.js +110 -52
- package/lib/trusted.js +44 -0
- package/lib/updates.js +1 -38
- package/lib/witness.js +34 -65
- package/lib/writers.js +180 -116
- package/package.json +5 -4
- package/lib/reboot.js +0 -238
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
const c = require('compact-encoding')
|
|
2
|
+
const b4a = require('b4a')
|
|
3
|
+
const crypto = require('hypercore-crypto')
|
|
4
|
+
const { AutobeeEncryption } = require('autobee-encryption')
|
|
5
|
+
const { decodeBlock } = require('hyperbee2/lib/encoding.js')
|
|
6
|
+
|
|
7
|
+
const encoding = require('./encoding.js')
|
|
8
|
+
const { assert, bail } = require('./asserts.js')
|
|
9
|
+
const { LEGACY_OPLOG_VERSION } = require('./constants.js')
|
|
10
|
+
|
|
11
|
+
const { getEncoding } = require('../encoding/spec/autobee')
|
|
12
|
+
|
|
13
|
+
const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
|
|
14
|
+
const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
|
|
15
|
+
|
|
16
|
+
const EMPTY = b4a.alloc(0)
|
|
17
|
+
const INDEX_VERSION = 1
|
|
18
|
+
const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
19
|
+
|
|
20
|
+
// indexer rotations chain legacy systems - how many generations we chase
|
|
21
|
+
const MAX_MIGRATE_HOPS = 64
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
MAX_MIGRATE_HOPS,
|
|
25
|
+
checkAutobaseMigration,
|
|
26
|
+
readLegacySystemInfo,
|
|
27
|
+
findNewestSystem,
|
|
28
|
+
resolveLegacyViews,
|
|
29
|
+
inflateLegacyOplog,
|
|
30
|
+
decodeLegacySystemInfo,
|
|
31
|
+
deriveNamespace,
|
|
32
|
+
coreLength
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// boot-time migration of local legacy autobase storage
|
|
36
|
+
async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
|
|
37
|
+
const bootRecord = await local.getUserData('autobase/boot')
|
|
38
|
+
if (!bootRecord) return null
|
|
39
|
+
|
|
40
|
+
const { key, systemLength } = encoding.decodeAutobaseBootRecord(bootRecord)
|
|
41
|
+
const core = store.get({ key: key, encryption: null })
|
|
42
|
+
await core.ready()
|
|
43
|
+
|
|
44
|
+
const length = await findLocalSystemLength(core, systemLength)
|
|
45
|
+
assert(length > 0, 'Expected system block to exist locally')
|
|
46
|
+
|
|
47
|
+
const system = { key, length }
|
|
48
|
+
|
|
49
|
+
// setup encryption
|
|
50
|
+
const encryptionKey = await local.getUserData('autobase/encryption')
|
|
51
|
+
await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
|
|
52
|
+
|
|
53
|
+
const session = core.session({ name: 'batch' })
|
|
54
|
+
await session.ready()
|
|
55
|
+
|
|
56
|
+
let catchup
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
catchup = await getCatchupHeads(session, length)
|
|
60
|
+
} finally {
|
|
61
|
+
await session.close()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const info = await readLegacySystemInfo(core, length, { wait: false })
|
|
65
|
+
|
|
66
|
+
const nodes = await Promise.all(
|
|
67
|
+
catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const indexerManifests = await Promise.all(
|
|
71
|
+
info.indexers.map((idx) => storeCoreManifest(store, idx.key))
|
|
72
|
+
)
|
|
73
|
+
const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
|
|
74
|
+
const entropy = legacyEntropy(info, indexerManifests)
|
|
75
|
+
|
|
76
|
+
const getManifest = (key) => storeCoreManifest(store, key)
|
|
77
|
+
|
|
78
|
+
const mappedViews = new Map()
|
|
79
|
+
for (const name of legacyViews) {
|
|
80
|
+
const v = await findViewByName(
|
|
81
|
+
getManifest,
|
|
82
|
+
bootstrap.key,
|
|
83
|
+
encryptionKey,
|
|
84
|
+
indexerManifests,
|
|
85
|
+
views,
|
|
86
|
+
entropy,
|
|
87
|
+
name
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
mappedViews.set(name, v)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await core.close()
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
encryptionKey,
|
|
97
|
+
system,
|
|
98
|
+
views: mappedViews,
|
|
99
|
+
catchup: nodes
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// decode the legacy system info recorded in the system core's block at length
|
|
104
|
+
async function readLegacySystemInfo(core, length, opts = null) {
|
|
105
|
+
const block = await core.get(length - 1, opts)
|
|
106
|
+
assert(block !== null, 'Expected system block to exist locally')
|
|
107
|
+
|
|
108
|
+
// Decode hyperbee block using hyperbee2 compat
|
|
109
|
+
const node = decodeBlock(block)
|
|
110
|
+
assert(node.keys.length > 0, 'bad system block')
|
|
111
|
+
|
|
112
|
+
// Decode system info from hyperbee block using autobee compat
|
|
113
|
+
return decodeLegacySystemInfo(node.keys[0].value)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
|
|
117
|
+
// the newest legacy system that indexer knows. take the majority vote. an
|
|
118
|
+
// indexer that already moved to autobee announces its live autobee state
|
|
119
|
+
// instead - keep the newest of those as a direct fast-forward candidate and
|
|
120
|
+
// bisect down to its buried legacy stamps so its vote still counts
|
|
121
|
+
async function findNewestSystem(ff, info) {
|
|
122
|
+
const tally = new Map()
|
|
123
|
+
|
|
124
|
+
let autobee = null
|
|
125
|
+
|
|
126
|
+
for (const idx of info.indexers) {
|
|
127
|
+
const core = ff.auto.openCore(idx.key)
|
|
128
|
+
ff.cores.push(core)
|
|
129
|
+
await core.ready()
|
|
130
|
+
|
|
131
|
+
const length = await coreLength(core, ff.timeout)
|
|
132
|
+
if (!length || ff.destroyed) continue
|
|
133
|
+
|
|
134
|
+
let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
|
|
135
|
+
if (oplog === null || !oplog.op.views) continue
|
|
136
|
+
|
|
137
|
+
if (oplog.op.version > LEGACY_OPLOG_VERSION) {
|
|
138
|
+
if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
|
|
139
|
+
autobee = oplog
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
|
|
143
|
+
if (!boundary || ff.destroyed) continue
|
|
144
|
+
|
|
145
|
+
oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
|
|
146
|
+
if (oplog === null || !oplog.op.views) continue
|
|
147
|
+
if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sys = oplog.op.views.system
|
|
151
|
+
if (!sys || !sys.key) continue
|
|
152
|
+
|
|
153
|
+
const hex = b4a.toString(sys.key, 'hex')
|
|
154
|
+
const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
|
|
155
|
+
|
|
156
|
+
t.votes++
|
|
157
|
+
if (sys.length > t.length) t.length = sys.length
|
|
158
|
+
tally.set(hex, t)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const majority = (info.indexers.length >> 1) + 1
|
|
162
|
+
|
|
163
|
+
let best = null
|
|
164
|
+
for (const t of tally.values()) {
|
|
165
|
+
if (t.votes >= majority && (best === null || t.length > best.length)) best = t
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { legacy: best === null ? null : best.key, autobee }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// map the legacy views recorded in the system info by name
|
|
172
|
+
async function resolveLegacyViews(ff, info) {
|
|
173
|
+
const indexerManifests = await Promise.all(
|
|
174
|
+
info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
|
|
175
|
+
)
|
|
176
|
+
if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
|
|
177
|
+
|
|
178
|
+
const entropy = legacyEntropy(info, indexerManifests)
|
|
179
|
+
|
|
180
|
+
const getManifest = (key, length) => ffCoreManifest(ff, key, length)
|
|
181
|
+
|
|
182
|
+
const views = new Map()
|
|
183
|
+
|
|
184
|
+
await Promise.all(
|
|
185
|
+
ff.auto.legacyViews.map(async (name) => {
|
|
186
|
+
const v = await findViewByName(
|
|
187
|
+
getManifest,
|
|
188
|
+
ff.auto.key,
|
|
189
|
+
ff.auto.encryptionKey,
|
|
190
|
+
indexerManifests,
|
|
191
|
+
info.views,
|
|
192
|
+
entropy,
|
|
193
|
+
name
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if (v) views.set(name, v)
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if (ff.destroyed) return null
|
|
201
|
+
|
|
202
|
+
return views
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// legacy nodes always inflate, but only indexers carry the digest/checkpoint
|
|
206
|
+
// stamps that synthesise system info - callers must check op.views
|
|
207
|
+
async function inflateLegacyOplog(buf, core, seq, timeout) {
|
|
208
|
+
const m = encoding.decodeRawOplog(buf)
|
|
209
|
+
|
|
210
|
+
const op = {
|
|
211
|
+
version: m.version,
|
|
212
|
+
timestamp: 0,
|
|
213
|
+
links: m.node.heads,
|
|
214
|
+
batch: { start: 0, end: m.node.batch - 1 },
|
|
215
|
+
views: null,
|
|
216
|
+
optimistic: !!m.optimistic,
|
|
217
|
+
value: m.node.value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
|
|
221
|
+
|
|
222
|
+
const fetches = []
|
|
223
|
+
|
|
224
|
+
fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, { timeout }) : buf)
|
|
225
|
+
fetches.push(
|
|
226
|
+
m.checkpoint.system.checkpointer
|
|
227
|
+
? core.get(seq - m.checkpoint.system.checkpointer, { timeout })
|
|
228
|
+
: buf
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const [digestNode, checkpointNode] = await Promise.all(fetches)
|
|
232
|
+
// no caller reads best-effort today, but stay total over a missing block
|
|
233
|
+
if (digestNode === null || checkpointNode === null) return op
|
|
234
|
+
|
|
235
|
+
const { digest } = encoding.decodeRawOplog(digestNode)
|
|
236
|
+
const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
|
|
237
|
+
|
|
238
|
+
if (!checkpoint.system || !checkpoint.system.checkpoint) return op
|
|
239
|
+
|
|
240
|
+
op.views = {
|
|
241
|
+
system: {
|
|
242
|
+
key: digest.key,
|
|
243
|
+
start: 0,
|
|
244
|
+
length: checkpoint.system.checkpoint.length
|
|
245
|
+
},
|
|
246
|
+
flushes: seq
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return op
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function decodeLegacySystemInfo(buffer) {
|
|
253
|
+
const state = { start: 0, end: buffer.length, buffer }
|
|
254
|
+
const version = c.uint.decode(state)
|
|
255
|
+
|
|
256
|
+
state.start--
|
|
257
|
+
|
|
258
|
+
switch (version) {
|
|
259
|
+
case 1:
|
|
260
|
+
return SystemInfoV1.decode(state)
|
|
261
|
+
case 2:
|
|
262
|
+
return SystemInfoV2.decode(state)
|
|
263
|
+
default:
|
|
264
|
+
bail('Expected legacy system info')
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
|
|
269
|
+
const encryptionId = crypto.hash(encryptionKey || EMPTY)
|
|
270
|
+
const version = c.encode(c.uint, INDEX_VERSION)
|
|
271
|
+
|
|
272
|
+
return crypto.hash([
|
|
273
|
+
NS_SIGNER_NAMESPACE,
|
|
274
|
+
version,
|
|
275
|
+
bootstrap,
|
|
276
|
+
encryptionId,
|
|
277
|
+
entropy,
|
|
278
|
+
b4a.from(name)
|
|
279
|
+
])
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// the record's length can run past the signed core into the local batch
|
|
283
|
+
// session, and the signed core can carry verified-but-undownloaded blocks -
|
|
284
|
+
// scan back for the newest block the signed core actually has locally
|
|
285
|
+
async function findLocalSystemLength(core, systemLength) {
|
|
286
|
+
let length = Math.min(core.length, systemLength)
|
|
287
|
+
while (length > 0 && !(await core.has(length - 1))) length--
|
|
288
|
+
return length
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function coreLength(core, timeout) {
|
|
292
|
+
if (core.length) return core.length
|
|
293
|
+
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
core.on('append', () => resolve(core.length))
|
|
296
|
+
setTimeout(resolve, timeout, 0)
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// a writer's oplog is a legacy prefix followed by autobee-format nodes -
|
|
301
|
+
// bisect for the boundary. lo is a length known to be legacy (the legacy
|
|
302
|
+
// system's recorded length for this writer), hi a seq known to be autobee.
|
|
303
|
+
// returns the legacy prefix length
|
|
304
|
+
async function findLegacyBoundary(ff, core, lo, hi) {
|
|
305
|
+
while (lo < hi) {
|
|
306
|
+
const mid = (lo + hi) >>> 1
|
|
307
|
+
|
|
308
|
+
const block = await core.get(mid, {
|
|
309
|
+
timeout: ff.timeout,
|
|
310
|
+
activeRequests: ff.activeRequests
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
|
|
314
|
+
else lo = mid + 1
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return lo
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function legacyEntropy(info, indexerManifests) {
|
|
321
|
+
return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function findViewByName(
|
|
325
|
+
getManifest,
|
|
326
|
+
bootstrap,
|
|
327
|
+
encryptionKey,
|
|
328
|
+
indexerManifests,
|
|
329
|
+
views,
|
|
330
|
+
entropy,
|
|
331
|
+
name
|
|
332
|
+
) {
|
|
333
|
+
if (indexerManifests.length === 0) return null
|
|
334
|
+
|
|
335
|
+
const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
|
|
336
|
+
|
|
337
|
+
for (const v of views) {
|
|
338
|
+
const manifest = await getManifest(v.key, v.length)
|
|
339
|
+
if (!manifest) continue
|
|
340
|
+
|
|
341
|
+
if (manifest.signers.length === 0) continue
|
|
342
|
+
|
|
343
|
+
const signer = manifest.signers[0]
|
|
344
|
+
|
|
345
|
+
if (b4a.equals(signer.namespace, namespace)) return v
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// request-tracked manifest read over replication
|
|
352
|
+
async function ffCoreManifest(ff, key, length) {
|
|
353
|
+
const core = ff.auto.store.get(key)
|
|
354
|
+
ff.cores.push(core)
|
|
355
|
+
|
|
356
|
+
await core.ready()
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
if (!core.manifest) {
|
|
360
|
+
await core.get(length - 1, {
|
|
361
|
+
timeout: ff.timeout,
|
|
362
|
+
activeRequests: ff.activeRequests
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return core.manifest
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// local-storage manifest read
|
|
373
|
+
async function storeCoreManifest(store, key) {
|
|
374
|
+
const core = store.get(key)
|
|
375
|
+
await core.ready()
|
|
376
|
+
|
|
377
|
+
const manifest = core.manifest
|
|
378
|
+
await core.close()
|
|
379
|
+
|
|
380
|
+
return manifest
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function getPersistedView(store, view) {
|
|
384
|
+
const core = store.get(view.key)
|
|
385
|
+
await core.ready()
|
|
386
|
+
|
|
387
|
+
// core.length can run past what was downloaded - the system's recorded
|
|
388
|
+
// length is the last one we know is local
|
|
389
|
+
const length = Math.min(core.length, view.length)
|
|
390
|
+
await core.close()
|
|
391
|
+
|
|
392
|
+
return { key: view.key, length }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function getCatchupHeads(session, from) {
|
|
396
|
+
const seen = new Map()
|
|
397
|
+
const nodes = []
|
|
398
|
+
|
|
399
|
+
// block 0 is the bee header, never a node
|
|
400
|
+
for (let i = Math.max(from, 1); i < session.length; i++) {
|
|
401
|
+
const node = await session.get(i, { wait: false })
|
|
402
|
+
if (!node) throw new Error('Expect nodes to exist locally')
|
|
403
|
+
|
|
404
|
+
const entry = decodeBlock(node).keys[0]
|
|
405
|
+
if (entry.key[0] !== 0x01) continue
|
|
406
|
+
|
|
407
|
+
const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
|
|
408
|
+
if (!length) continue
|
|
409
|
+
|
|
410
|
+
const id = b4a.toString(key, 'hex')
|
|
411
|
+
const current = seen.get(id) || 0
|
|
412
|
+
|
|
413
|
+
if (current >= length) continue
|
|
414
|
+
|
|
415
|
+
seen.set(id, length)
|
|
416
|
+
nodes.push({ key, length })
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return nodes
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
|
|
423
|
+
const batch = []
|
|
424
|
+
|
|
425
|
+
const core = store.get(head.key)
|
|
426
|
+
await core.ready()
|
|
427
|
+
|
|
428
|
+
if (encryptionKey) {
|
|
429
|
+
await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let seq = head.length - 1
|
|
433
|
+
const block = await core.get(seq--, { wait: false })
|
|
434
|
+
if (!block) throw new Error('Expect writer node to exist locally')
|
|
435
|
+
|
|
436
|
+
const node = encoding.decodeOplog(block)
|
|
437
|
+
|
|
438
|
+
batch.unshift({ ...head, ...node, from: core })
|
|
439
|
+
|
|
440
|
+
while (seq >= 0) {
|
|
441
|
+
const block = await core.get(seq--, { wait: false })
|
|
442
|
+
if (!block) break
|
|
443
|
+
|
|
444
|
+
const node = encoding.decodeOplog(block)
|
|
445
|
+
if (!node.batch.end) break
|
|
446
|
+
|
|
447
|
+
batch.unshift({ key: head.key, length: seq + 2, from: core, ...node })
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return batch
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// lazy to avoid the require cycle - fast-forward imports this module
|
|
454
|
+
function flushHead(auto, head, opts) {
|
|
455
|
+
const FastForward = require('./fast-forward.js')
|
|
456
|
+
return FastForward.flushHead(auto, head, opts)
|
|
457
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const b4a = require('b4a')
|
|
2
|
+
const encoding = require('./encoding.js')
|
|
3
|
+
|
|
4
|
+
// Owns the two promotion indexes: one hint per writer (the coordinate of the
|
|
5
|
+
// approval that anchored its capability - advisory, never verified against)
|
|
6
|
+
// and one pending entry per writer awaiting a stronger approval.
|
|
7
|
+
//
|
|
8
|
+
// An approval clears the entry as soon as it satisfies the request, so the
|
|
9
|
+
// index holds exactly the outstanding work. A partial approval leaves the
|
|
10
|
+
// entry alone: the request still wants more than the tier that answered could
|
|
11
|
+
// give.
|
|
12
|
+
//
|
|
13
|
+
// digest[w - 1] is true when some request needs an approver of standing w.
|
|
14
|
+
// The false is what makes a tier go quiet: once one tier-w peer has anchored
|
|
15
|
+
// w, the rest see their tier is finished instead of all offering in turn. A
|
|
16
|
+
// flag is enough because a satisfied request leaves the index entirely, so
|
|
17
|
+
// there is no "have I already seen this" watermark to keep.
|
|
18
|
+
module.exports = class Promotions {
|
|
19
|
+
constructor(bee) {
|
|
20
|
+
this.bee = bee
|
|
21
|
+
this.digest = []
|
|
22
|
+
this.changed = false
|
|
23
|
+
this._pending = null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
reset(info) {
|
|
27
|
+
this.digest = info && info.pending ? info.pending : []
|
|
28
|
+
this._pending = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async hint(key, { activeRequests } = {}) {
|
|
32
|
+
const node = await this.bee.get(encoding.encodeSystemGrantKey(key), { activeRequests })
|
|
33
|
+
return node ? encoding.decodeSystemGrantHint(node.value) : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async pending(key, { activeRequests } = {}) {
|
|
37
|
+
if (this._pending !== null) {
|
|
38
|
+
const entry = this._pending.get(b4a.toString(key, 'hex'))
|
|
39
|
+
return entry ? entry.weight : 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const node = await this.bee.get(encoding.encodeSystemPendingKey(key), { activeRequests })
|
|
43
|
+
return node ? encoding.decodeSystemPending(node.value).weight : 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// the level a writer has already anchored is exactly what its hint records,
|
|
47
|
+
// so it is read from there rather than mirrored into the pending entry
|
|
48
|
+
async anchored(key, { activeRequests } = {}) {
|
|
49
|
+
const hint = await this.hint(key, { activeRequests })
|
|
50
|
+
return hint ? hint.weight : 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async *list({ activeRequests } = {}) {
|
|
54
|
+
const pending = await this._load({ activeRequests })
|
|
55
|
+
for (const [hex, entry] of pending) {
|
|
56
|
+
yield { key: b4a.from(hex, 'hex'), weight: entry.weight }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async updateHint(upd) {
|
|
61
|
+
if (!upd.grant && !upd.isRemoved) return null
|
|
62
|
+
|
|
63
|
+
const key = encoding.encodeSystemGrantKey(upd.key)
|
|
64
|
+
const node = await this.bee.get(key)
|
|
65
|
+
|
|
66
|
+
if (upd.isRemoved) return node ? { key, remove: true } : null
|
|
67
|
+
|
|
68
|
+
const existing = node ? encoding.decodeSystemGrantHint(node.value) : null
|
|
69
|
+
if (existing && existing.weight >= upd.grant.weight) return null
|
|
70
|
+
|
|
71
|
+
return { key, value: encoding.encodeSystemGrantHint(upd.grant) }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// grouped and sequential on purpose: the entries are shared state, so
|
|
75
|
+
// per-update parallelism would interleave at the awaits
|
|
76
|
+
async flushPending(updates) {
|
|
77
|
+
let relevant = false
|
|
78
|
+
for (const upd of updates) {
|
|
79
|
+
if (upd.pending !== -1 || upd.grant !== null || upd.isRemoved) {
|
|
80
|
+
relevant = true
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!relevant) return null
|
|
85
|
+
|
|
86
|
+
const pending = await this._load()
|
|
87
|
+
if (!pending.size && !updates.some((u) => u.pending !== -1)) return null
|
|
88
|
+
|
|
89
|
+
const ops = []
|
|
90
|
+
const anchoredBy = new Map()
|
|
91
|
+
|
|
92
|
+
for (const upd of updates) {
|
|
93
|
+
const hex = b4a.toString(upd.key, 'hex')
|
|
94
|
+
const existing = pending.get(hex) || null
|
|
95
|
+
|
|
96
|
+
if (upd.isRemoved) {
|
|
97
|
+
if (existing) {
|
|
98
|
+
pending.delete(hex)
|
|
99
|
+
ops.push({ key: encoding.encodeSystemPendingKey(upd.key), remove: true })
|
|
100
|
+
}
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (upd.pending === -1 && upd.grant === null) continue
|
|
104
|
+
|
|
105
|
+
// the hint is still pre-write here, so this is the level already anchored
|
|
106
|
+
// before whatever this update confers
|
|
107
|
+
const prev = await this.anchored(upd.key)
|
|
108
|
+
const anchored = upd.grant && upd.grant.weight > prev ? upd.grant.weight : prev
|
|
109
|
+
anchoredBy.set(hex, anchored)
|
|
110
|
+
|
|
111
|
+
const want = Math.max(existing ? existing.weight : 0, upd.pending)
|
|
112
|
+
|
|
113
|
+
if (want > anchored) {
|
|
114
|
+
if (existing && want === existing.weight) continue
|
|
115
|
+
const entry = { weight: want }
|
|
116
|
+
pending.set(hex, entry)
|
|
117
|
+
ops.push({
|
|
118
|
+
key: encoding.encodeSystemPendingKey(upd.key),
|
|
119
|
+
value: encoding.encodeSystemPending(entry)
|
|
120
|
+
})
|
|
121
|
+
this.changed = true
|
|
122
|
+
} else if (existing) {
|
|
123
|
+
// satisfied - the approval covered everything the request asked for
|
|
124
|
+
pending.delete(hex)
|
|
125
|
+
ops.push({ key: encoding.encodeSystemPendingKey(upd.key), remove: true })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// the flags depend on the anchored level, not just on which entries exist -
|
|
130
|
+
// an approval that partially serves a request writes no entry at all, and
|
|
131
|
+
// its tier still has to go quiet
|
|
132
|
+
const open = new Set()
|
|
133
|
+
for (const [hex, entry] of pending) {
|
|
134
|
+
const anchored = anchoredBy.has(hex)
|
|
135
|
+
? anchoredBy.get(hex)
|
|
136
|
+
: await this.anchored(b4a.from(hex, 'hex'))
|
|
137
|
+
for (let w = anchored + 1; w <= entry.weight; w++) open.add(w)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let max = 0
|
|
141
|
+
for (const w of open) if (w > max) max = w
|
|
142
|
+
|
|
143
|
+
this.digest = []
|
|
144
|
+
for (let w = 1; w <= max; w++) this.digest.push(open.has(w))
|
|
145
|
+
|
|
146
|
+
return ops.length ? ops : null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async _load({ activeRequests } = {}) {
|
|
150
|
+
if (this._pending !== null) return this._pending
|
|
151
|
+
|
|
152
|
+
const pending = new Map()
|
|
153
|
+
const range = encoding.encodeSystemPendingRange()
|
|
154
|
+
for await (const data of this.bee.createReadStream({ ...range, activeRequests })) {
|
|
155
|
+
pending.set(
|
|
156
|
+
b4a.toString(encoding.decodeSystemPendingKey(data.key), 'hex'),
|
|
157
|
+
encoding.decodeSystemPending(data.value)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this._pending = pending
|
|
162
|
+
return pending
|
|
163
|
+
}
|
|
164
|
+
}
|