@subsquid/batch-processor 1.0.0-portal-api.a3f844 → 1.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/lib/database.d.ts +25 -8
- package/lib/database.d.ts.map +1 -1
- package/lib/run.d.ts +25 -11
- package/lib/run.d.ts.map +1 -1
- package/lib/run.js +183 -149
- package/lib/run.js.map +1 -1
- package/lib/util.d.ts +2 -2
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +1 -1
- package/lib/util.js.map +1 -1
- package/package.json +12 -9
- package/src/database.ts +28 -13
- package/src/find-rollback-index.test.ts +85 -0
- package/src/run.ts +219 -152
- package/src/test/processor.test.ts +651 -0
- package/src/util.ts +3 -3
- package/lib/metrics.d.ts +0 -21
- package/lib/metrics.d.ts.map +0 -1
- package/lib/metrics.js +0 -92
- package/lib/metrics.js.map +0 -1
- package/src/metrics.ts +0 -111
package/src/run.ts
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
import {createLogger} from '@subsquid/logger'
|
|
2
2
|
import {last, maybeLast, runProgram, Throttler} from '@subsquid/util-internal'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {HashAndHeight, Database, HotDatabaseState} from './database'
|
|
6
|
-
import {Metrics} from './metrics'
|
|
7
|
-
import {DataSource, isForkException, BlockRef} from '@subsquid/util-internal-data-source'
|
|
3
|
+
import {Database, DatabaseTransactResult, FinalDatabaseState, HashAndHeight, HotDatabaseState} from './database'
|
|
4
|
+
import {DataSource, isForkException, BlockRef, type BlockBatch} from '@subsquid/util-internal-data-source'
|
|
8
5
|
import assert from 'assert'
|
|
6
|
+
import {PrometheusServer, RunnerMetrics} from '@subsquid/util-internal-processor-tools'
|
|
9
7
|
import {formatHead, getItemsCount} from './util'
|
|
10
8
|
|
|
9
|
+
export {PrometheusServer}
|
|
10
|
+
|
|
11
11
|
const log = createLogger('sqd:batch-processor')
|
|
12
12
|
|
|
13
13
|
export interface DataHandlerContext<Block, Store> {
|
|
14
|
-
/**
|
|
15
|
-
* Storage interface provided by the database
|
|
16
|
-
*/
|
|
17
14
|
store: Store
|
|
18
|
-
/**
|
|
19
|
-
* List of blocks to map and process
|
|
20
|
-
*/
|
|
21
15
|
blocks: Block[]
|
|
22
|
-
/**
|
|
23
|
-
* Signals, that the processor is near the head of the chain.
|
|
24
|
-
*/
|
|
25
16
|
isHead: boolean
|
|
26
17
|
}
|
|
27
18
|
|
|
@@ -29,6 +20,10 @@ export interface BlockBase {
|
|
|
29
20
|
header: BlockRef
|
|
30
21
|
}
|
|
31
22
|
|
|
23
|
+
export interface RunOptions {
|
|
24
|
+
prometheus?: PrometheusServer
|
|
25
|
+
}
|
|
26
|
+
|
|
32
27
|
/**
|
|
33
28
|
* Run data processing.
|
|
34
29
|
*
|
|
@@ -46,183 +41,227 @@ export interface BlockBase {
|
|
|
46
41
|
export function run<Block extends BlockBase, Store>(
|
|
47
42
|
src: DataSource<Block>,
|
|
48
43
|
db: Database<Store>,
|
|
49
|
-
dataHandler: (ctx: DataHandlerContext<Block, Store>) => Promise<void
|
|
44
|
+
dataHandler: (ctx: DataHandlerContext<Block, Store>) => Promise<DatabaseTransactResult | void>,
|
|
45
|
+
opts?: RunOptions,
|
|
50
46
|
): void {
|
|
51
47
|
runProgram(
|
|
52
48
|
() => {
|
|
53
|
-
return new Processor(src, db, dataHandler).run()
|
|
49
|
+
return new Processor(src, db, dataHandler, opts).run()
|
|
54
50
|
},
|
|
55
51
|
(err) => {
|
|
56
52
|
log.fatal(err)
|
|
57
|
-
}
|
|
53
|
+
},
|
|
58
54
|
)
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
interface ProcessorStateInit {
|
|
58
|
+
finalizedHead?: BlockRef
|
|
59
|
+
unfinalizedHeads?: BlockRef[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class ProcessorState {
|
|
63
|
+
finalizedHead: BlockRef | undefined = undefined
|
|
64
|
+
unfinalizedHeads: BlockRef[] = []
|
|
65
|
+
|
|
66
|
+
get head(): BlockRef | undefined {
|
|
67
|
+
return maybeLast(this.unfinalizedHeads) ?? this.finalizedHead
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
init(state: ProcessorStateInit): void {
|
|
71
|
+
this.finalizedHead = state.finalizedHead
|
|
72
|
+
this.unfinalizedHeads = state.unfinalizedHeads ?? []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
handleFork(previousBlocks: BlockRef[]): void {
|
|
76
|
+
let chain = this.finalizedHead ? [this.finalizedHead, ...this.unfinalizedHeads] : this.unfinalizedHeads
|
|
77
|
+
let rollbackIndex = findRollbackIndex(chain, previousBlocks)
|
|
78
|
+
if (rollbackIndex === -1) {
|
|
79
|
+
if (this.finalizedHead != null) throw new Error('Unable to process fork')
|
|
80
|
+
this.unfinalizedHeads = []
|
|
81
|
+
} else {
|
|
82
|
+
let rollbackHead = chain[rollbackIndex]
|
|
83
|
+
log.info(`navigating a fork on a common base ${formatHead(rollbackHead)}`)
|
|
84
|
+
this.unfinalizedHeads = chain.slice(this.finalizedHead ? 1 : 0, rollbackIndex + 1)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class Processor<B extends BlockBase, S> {
|
|
90
|
+
private metrics: RunnerMetrics
|
|
64
91
|
private statusReportTimer?: any
|
|
65
92
|
private hasStatusNews = false
|
|
93
|
+
private state = new ProcessorState()
|
|
66
94
|
|
|
67
95
|
constructor(
|
|
68
96
|
private src: DataSource<B>,
|
|
69
97
|
private db: Database<S>,
|
|
70
|
-
private handler: (ctx: DataHandlerContext<B, S>) => Promise<void
|
|
98
|
+
private handler: (ctx: DataHandlerContext<B, S>) => Promise<DatabaseTransactResult | void>,
|
|
99
|
+
private readonly opts?: RunOptions,
|
|
71
100
|
) {
|
|
72
|
-
this.
|
|
101
|
+
this.metrics = new RunnerMetrics(
|
|
102
|
+
src.getBlocksCountInRange?.bind(src) ?? ((range) => Math.max(0, range.to - range.from + 1)),
|
|
103
|
+
)
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
async run(): Promise<void> {
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
finalizedHead = state
|
|
81
|
-
head = last([state, ...state.top])
|
|
82
|
-
} else {
|
|
83
|
-
finalizedHead = head = await this.db.connect()
|
|
84
|
-
}
|
|
107
|
+
let getHead = this.db.supportsHotBlocks
|
|
108
|
+
? this.src.getHead.bind(this.src)
|
|
109
|
+
: this.src.getFinalizedHead.bind(this.src)
|
|
110
|
+
let chainHeight = new Throttler(() => getHead().then((r) => r.number), 10_000)
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
112
|
+
let dbState = await this.db.connect()
|
|
113
|
+
this.state.init(toProcessorStateInit(dbState))
|
|
114
|
+
|
|
115
|
+
let head = this.state.head
|
|
116
|
+
if (head != null) {
|
|
117
|
+
log.info(`last processed block was ${head.number}`)
|
|
89
118
|
}
|
|
119
|
+
await this.initMetrics(head?.number ?? -1, await chainHeight.get())
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
let getStream = this.db.supportsHotBlocks
|
|
122
|
+
? this.src.getStream.bind(this.src)
|
|
123
|
+
: this.src.getFinalizedStream.bind(this.src)
|
|
92
124
|
|
|
93
125
|
while (true) {
|
|
94
126
|
try {
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
: {
|
|
107
|
-
height: data.finalizedHead.number,
|
|
108
|
-
hash: data.finalizedHead.hash,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
head = await this.processBatch(head, finalizedHead, data.blocks)
|
|
127
|
+
for await (let data of getStream({
|
|
128
|
+
from: (this.state.head?.number ?? -1) + 1,
|
|
129
|
+
parentHash: this.state.head?.hash,
|
|
130
|
+
})) {
|
|
131
|
+
await this.processBatch(
|
|
132
|
+
data,
|
|
133
|
+
await chainHeight.get(),
|
|
134
|
+
async (store: S, sliceBlocks: B[], isOnTop: boolean) => {
|
|
135
|
+
return this.handler({store, blocks: sliceBlocks, isHead: isOnTop})
|
|
136
|
+
},
|
|
137
|
+
)
|
|
112
138
|
}
|
|
113
|
-
|
|
114
139
|
break
|
|
115
140
|
} catch (e) {
|
|
116
|
-
if (isForkException(e)
|
|
117
|
-
|
|
118
|
-
let forkBase = await computeForkBase(state, e.previousBlocks)
|
|
119
|
-
if (forkBase == null) {
|
|
120
|
-
// rollback all blocks
|
|
121
|
-
head = {height: -1, hash: '0x'}
|
|
122
|
-
} else {
|
|
123
|
-
head = forkBase
|
|
124
|
-
}
|
|
125
|
-
log.info(`navigating a fork on a common base ${formatHead(head)}`)
|
|
126
|
-
} else {
|
|
127
|
-
throw e
|
|
128
|
-
}
|
|
141
|
+
if (!isForkException(e) || !this.db.supportsHotBlocks) throw e
|
|
142
|
+
this.state.handleFork(e.previousBlocks)
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
this.reportFinalStatus()
|
|
133
147
|
}
|
|
134
148
|
|
|
135
|
-
private async
|
|
136
|
-
|
|
137
|
-
// let hash = await this.src.getBlockHash(state.number)
|
|
138
|
-
// if (state.hash === hash) return
|
|
139
|
-
// throw new Error(
|
|
140
|
-
// `already indexed block ${formatHead(state)} was not found on chain`
|
|
141
|
-
// )
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private async initMetrics(state: number): Promise<void> {
|
|
145
|
-
this.updateProgressMetrics(await this.chainHeight.get(), state)
|
|
149
|
+
private async initMetrics(state: number, chainHeight: number): Promise<void> {
|
|
150
|
+
this.updateProgressMetrics(chainHeight, state)
|
|
146
151
|
let port = process.env.PROCESSOR_PROMETHEUS_PORT || process.env.PROMETHEUS_PORT
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
|
|
153
|
+
let prometheusServer: PrometheusServer | undefined
|
|
154
|
+
if (this.opts?.prometheus != null) {
|
|
155
|
+
prometheusServer = this.opts.prometheus
|
|
156
|
+
} else if (port != null) {
|
|
157
|
+
prometheusServer = new PrometheusServer()
|
|
158
|
+
prometheusServer.setPort(port)
|
|
159
|
+
}
|
|
160
|
+
if (prometheusServer == null) return
|
|
161
|
+
|
|
162
|
+
prometheusServer.addRunnerMetrics(this.metrics)
|
|
163
|
+
let listening = await prometheusServer.serve()
|
|
164
|
+
log.info(`prometheus metrics are served on port ${listening.port}`)
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
private updateProgressMetrics(chainHeight: number, indexerHeight: number, time?: bigint): void {
|
|
155
168
|
this.metrics.setChainHeight(chainHeight)
|
|
156
169
|
this.metrics.setLastProcessedBlock(indexerHeight)
|
|
157
|
-
|
|
158
|
-
let processed: number
|
|
159
|
-
left = this.metrics.getChainHeight() - this.metrics.getLastProcessedBlock()
|
|
160
|
-
processed = this.metrics.getLastProcessedBlock()
|
|
161
|
-
this.metrics.updateProgress(processed, left, time)
|
|
170
|
+
this.metrics.updateProgress(time)
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
private async processBatch(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
blocks: B[]
|
|
168
|
-
): Promise<
|
|
169
|
-
let
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
let
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
data: BlockBatch<B>,
|
|
175
|
+
chainHeight: number,
|
|
176
|
+
map: (store: S, blocks: B[], isOnTop: boolean) => Promise<DatabaseTransactResult | void>,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
let {blocks, finalizedHead: finalizedHeadData} = data
|
|
179
|
+
let hasBlocks = blocks.length > 0
|
|
180
|
+
|
|
181
|
+
if (!hasBlocks && finalizedHeadData == null) return
|
|
182
|
+
|
|
183
|
+
let prevHead = this.state.head
|
|
184
|
+
|
|
185
|
+
if (!hasBlocks && !this.db.supportsHotBlocks) return
|
|
186
|
+
|
|
187
|
+
assertBlocksContinuity(prevHead, blocks)
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
finalizedHeadData != null &&
|
|
191
|
+
this.state.finalizedHead != null &&
|
|
192
|
+
finalizedHeadData.number <= this.state.finalizedHead.number
|
|
193
|
+
) {
|
|
194
|
+
finalizedHeadData = this.state.finalizedHead
|
|
177
195
|
}
|
|
196
|
+
finalizedHeadData = maxBlockRef(finalizedHeadData, this.state.finalizedHead)
|
|
197
|
+
|
|
198
|
+
let unfinalizedIndex =
|
|
199
|
+
finalizedHeadData == null ? 0 : blocks.findIndex((b) => b.header.number > finalizedHeadData.number)
|
|
200
|
+
unfinalizedIndex = unfinalizedIndex < 0 ? blocks.length : unfinalizedIndex
|
|
178
201
|
|
|
179
|
-
let
|
|
202
|
+
let nextHead = maybeLast(blocks)?.header ?? prevHead ?? finalizedHeadData
|
|
203
|
+
if (nextHead == null) return
|
|
204
|
+
|
|
205
|
+
let isOnTop = nextHead.number >= chainHeight
|
|
180
206
|
|
|
181
207
|
let mappingStartTime = process.hrtime.bigint()
|
|
182
208
|
|
|
183
209
|
if (this.db.supportsHotBlocks) {
|
|
210
|
+
let finalizedHead: BlockRef | undefined
|
|
211
|
+
if (!hasBlocks || unfinalizedIndex === 0) {
|
|
212
|
+
finalizedHead = finalizedHeadData
|
|
213
|
+
} else if (unfinalizedIndex === blocks.length) {
|
|
214
|
+
finalizedHead = last(blocks).header
|
|
215
|
+
} else {
|
|
216
|
+
finalizedHead = blocks[unfinalizedIndex - 1]?.header
|
|
217
|
+
}
|
|
218
|
+
let unfinalizedSliceHeads: BlockRef[] = []
|
|
184
219
|
await this.db.transactHot2(
|
|
185
220
|
{
|
|
186
|
-
finalizedHead,
|
|
187
|
-
baseHead: prevHead,
|
|
188
|
-
newBlocks: blocks.map((b) => (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
finalizedHead: toHashAndHeight(finalizedHead),
|
|
222
|
+
baseHead: toHashAndHeight(prevHead),
|
|
223
|
+
newBlocks: blocks.map((b) => toHashAndHeight(b.header)),
|
|
224
|
+
},
|
|
225
|
+
async (store, start, end) => {
|
|
226
|
+
let sliceBlocks = start === 0 && end === blocks.length ? blocks : blocks.slice(start, end)
|
|
227
|
+
if (sliceBlocks.length === 0) return
|
|
228
|
+
if (end > unfinalizedIndex) {
|
|
229
|
+
unfinalizedSliceHeads.push(last(sliceBlocks).header)
|
|
230
|
+
}
|
|
231
|
+
return map(store, sliceBlocks, isOnTop)
|
|
192
232
|
},
|
|
193
|
-
(store, start, end) => {
|
|
194
|
-
return this.handler({
|
|
195
|
-
store,
|
|
196
|
-
blocks: blocks.slice(start, end),
|
|
197
|
-
isHead: isOnTop,
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
233
|
)
|
|
234
|
+
|
|
235
|
+
let newFinalizedHead = finalizedHead ?? this.state.finalizedHead
|
|
236
|
+
let unfinalizedHeads = this.state.unfinalizedHeads
|
|
237
|
+
if (newFinalizedHead) {
|
|
238
|
+
let idx = unfinalizedHeads.findIndex((h) => h.number > newFinalizedHead.number)
|
|
239
|
+
unfinalizedHeads = idx < 0 ? [] : unfinalizedHeads.slice(idx)
|
|
240
|
+
}
|
|
241
|
+
this.state.finalizedHead = newFinalizedHead
|
|
242
|
+
this.state.unfinalizedHeads = unfinalizedHeads.concat(unfinalizedSliceHeads)
|
|
201
243
|
} else {
|
|
244
|
+
assert(unfinalizedIndex === blocks.length, 'non-hot database received unfinalized blocks')
|
|
245
|
+
|
|
202
246
|
await this.db.transact(
|
|
203
247
|
{
|
|
204
|
-
prevHead,
|
|
205
|
-
nextHead,
|
|
248
|
+
prevHead: toHashAndHeight(prevHead),
|
|
249
|
+
nextHead: toHashAndHeight(nextHead),
|
|
206
250
|
isOnTop,
|
|
207
251
|
},
|
|
208
|
-
(store) =>
|
|
209
|
-
return this.handler({
|
|
210
|
-
store,
|
|
211
|
-
blocks,
|
|
212
|
-
isHead: isOnTop,
|
|
213
|
-
})
|
|
214
|
-
}
|
|
252
|
+
(store) => map(store, blocks, isOnTop),
|
|
215
253
|
)
|
|
254
|
+
|
|
255
|
+
this.state.finalizedHead = nextHead
|
|
256
|
+
this.state.unfinalizedHeads = []
|
|
216
257
|
}
|
|
217
258
|
|
|
218
259
|
let mappingEndTime = process.hrtime.bigint()
|
|
219
260
|
|
|
220
|
-
this.updateProgressMetrics(chainHeight, nextHead.
|
|
261
|
+
this.updateProgressMetrics(chainHeight, nextHead.number, mappingEndTime)
|
|
221
262
|
this.metrics.registerBatch(blocks.length, getItemsCount(blocks), mappingStartTime, mappingEndTime)
|
|
222
263
|
|
|
223
264
|
this.reportStatus()
|
|
224
|
-
|
|
225
|
-
return nextHead
|
|
226
265
|
}
|
|
227
266
|
|
|
228
267
|
private reportStatus(): void {
|
|
@@ -234,7 +273,7 @@ class Processor<B extends BlockBase, S> {
|
|
|
234
273
|
this.hasStatusNews = false
|
|
235
274
|
this.reportStatus()
|
|
236
275
|
}
|
|
237
|
-
},
|
|
276
|
+
}, 5_000)
|
|
238
277
|
} else {
|
|
239
278
|
this.hasStatusNews = true
|
|
240
279
|
}
|
|
@@ -251,38 +290,66 @@ class Processor<B extends BlockBase, S> {
|
|
|
251
290
|
}
|
|
252
291
|
}
|
|
253
292
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
): Promise<HashAndHeight | undefined> {
|
|
259
|
-
assert(forked?.length)
|
|
260
|
-
let tail = forked.slice()
|
|
261
|
-
|
|
262
|
-
let commited = state.top
|
|
263
|
-
if (commited.length > 0) {
|
|
264
|
-
for (let i = commited.length - 1; i >= 0; i--) {
|
|
265
|
-
let h = commited[i]
|
|
293
|
+
export function findRollbackIndex(currentChain: BlockRef[], forkChain: BlockRef[]): number {
|
|
294
|
+
let currentIndex = 0
|
|
295
|
+
let forkIndex = 0
|
|
296
|
+
let lastCommonIndex = -1
|
|
266
297
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
298
|
+
while (currentIndex < currentChain.length && forkIndex < forkChain.length) {
|
|
299
|
+
const currentBlock = currentChain[currentIndex]
|
|
300
|
+
const forkBlock = forkChain[forkIndex]
|
|
270
301
|
|
|
271
|
-
|
|
302
|
+
if (currentBlock.number > forkBlock.number) {
|
|
303
|
+
forkIndex++
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
272
306
|
|
|
273
|
-
|
|
274
|
-
|
|
307
|
+
if (currentBlock.number < forkBlock.number) {
|
|
308
|
+
currentIndex++
|
|
309
|
+
continue
|
|
275
310
|
}
|
|
276
|
-
} else {
|
|
277
|
-
if (forked[0].number > state.height) return state
|
|
278
311
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (finalizedHead && finalizedHead.number >= state.height) {
|
|
282
|
-
throw new Error(`finalized block ${formatHead(state)} was not found on chain`)
|
|
283
|
-
}
|
|
312
|
+
if (currentBlock.hash !== forkBlock.hash) {
|
|
313
|
+
return lastCommonIndex
|
|
284
314
|
}
|
|
285
315
|
|
|
286
|
-
|
|
316
|
+
lastCommonIndex = currentIndex
|
|
317
|
+
currentIndex++
|
|
318
|
+
forkIndex++
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return lastCommonIndex
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function toHashAndHeight(ref: BlockRef | undefined): HashAndHeight {
|
|
325
|
+
if (ref == null) return {height: -1, hash: '0x'}
|
|
326
|
+
return {height: ref.number, hash: ref.hash}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function toBlockRef(hh: HashAndHeight): BlockRef {
|
|
330
|
+
return {number: hh.height, hash: hh.hash}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function toProcessorStateInit(dbState: FinalDatabaseState | HotDatabaseState): ProcessorStateInit {
|
|
334
|
+
let top = 'top' in dbState ? dbState.top : undefined
|
|
335
|
+
return {
|
|
336
|
+
finalizedHead: dbState.height < 0 ? undefined : toBlockRef(dbState),
|
|
337
|
+
unfinalizedHeads: top?.map((b) => toBlockRef(b)),
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function maxBlockRef(a: BlockRef | undefined, b: BlockRef | undefined): BlockRef | undefined {
|
|
342
|
+
if (a == null) return b
|
|
343
|
+
if (b == null) return a
|
|
344
|
+
return a.number >= b.number ? a : b
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function assertBlocksContinuity<B extends BlockBase>(prevHead: BlockRef | undefined, blocks: B[]): void {
|
|
348
|
+
let prev = prevHead
|
|
349
|
+
for (let block of blocks) {
|
|
350
|
+
if (prev && prev.number >= block.header.number) {
|
|
351
|
+
throw new Error('Data is not continuous')
|
|
352
|
+
}
|
|
353
|
+
prev = block.header
|
|
287
354
|
}
|
|
288
355
|
}
|