@sentio/runtime 0.0.0-rc.a
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/LICENSE +55 -0
- package/lib/chunk-DYOBLZD3.js +80341 -0
- package/lib/chunk-DYOBLZD3.js.map +1 -0
- package/lib/index.d.ts +663 -0
- package/lib/index.js +127 -0
- package/lib/index.js.map +1 -0
- package/lib/processor-runner.d.ts +34 -0
- package/lib/processor-runner.js +41155 -0
- package/lib/processor-runner.js.map +1 -0
- package/package.json +63 -0
- package/src/action-server.ts +18 -0
- package/src/chain-config.ts +5 -0
- package/src/db-context.ts +227 -0
- package/src/decode-benchmark.ts +28 -0
- package/src/endpoints.ts +11 -0
- package/src/full-service.ts +339 -0
- package/src/gen/google/protobuf/empty.ts +56 -0
- package/src/gen/google/protobuf/struct.ts +494 -0
- package/src/gen/google/protobuf/timestamp.ts +106 -0
- package/src/gen/processor/protos/processor.ts +13640 -0
- package/src/gen/service/common/protos/common.ts +14452 -0
- package/src/global-config.ts +33 -0
- package/src/index.ts +10 -0
- package/src/logger.ts +59 -0
- package/src/metrics.ts +202 -0
- package/src/multicall.ts +1615 -0
- package/src/otlp.ts +59 -0
- package/src/plugin.ts +120 -0
- package/src/processor-runner.ts +226 -0
- package/src/provider.ts +195 -0
- package/src/service-manager.ts +263 -0
- package/src/service-worker.ts +116 -0
- package/src/service.ts +505 -0
- package/src/state.ts +83 -0
- package/src/tsup.config.ts +16 -0
- package/src/utils.ts +93 -0
package/src/service.ts
ADDED
@@ -0,0 +1,505 @@
|
|
1
|
+
import { CallContext, ServerError, Status } from 'nice-grpc'
|
2
|
+
import { DebugInfo, RichServerError } from 'nice-grpc-error-details'
|
3
|
+
import { from } from 'ix/Ix.asynciterable'
|
4
|
+
import { withAbort } from 'ix/Ix.asynciterable.operators'
|
5
|
+
|
6
|
+
import {
|
7
|
+
DataBinding,
|
8
|
+
DeepPartial,
|
9
|
+
Empty,
|
10
|
+
EthCallParam,
|
11
|
+
HandlerType,
|
12
|
+
PreparedData,
|
13
|
+
PreprocessResult,
|
14
|
+
PreprocessStreamRequest,
|
15
|
+
PreprocessStreamResponse,
|
16
|
+
ProcessBindingResponse,
|
17
|
+
ProcessBindingsRequest,
|
18
|
+
ProcessConfigRequest,
|
19
|
+
ProcessConfigResponse,
|
20
|
+
ProcessorServiceImplementation,
|
21
|
+
ProcessResult,
|
22
|
+
ProcessStreamRequest,
|
23
|
+
ProcessStreamResponse,
|
24
|
+
StartRequest
|
25
|
+
} from '@sentio/protos'
|
26
|
+
|
27
|
+
import { PluginManager } from './plugin.js'
|
28
|
+
import { errorString, makeEthCallKey, mergeProcessResults } from './utils.js'
|
29
|
+
import { freezeGlobalConfig, GLOBAL_CONFIG } from './global-config.js'
|
30
|
+
|
31
|
+
import { StoreContext } from './db-context.js'
|
32
|
+
import { Subject } from 'rxjs'
|
33
|
+
import { getProvider } from './provider.js'
|
34
|
+
import { EthChainId } from '@sentio/chain'
|
35
|
+
import { Provider } from 'ethers'
|
36
|
+
import { decodeMulticallResult, encodeMulticallData, getMulticallAddress, Multicall3Call } from './multicall.js'
|
37
|
+
|
38
|
+
import { processMetrics } from './metrics.js'
|
39
|
+
|
40
|
+
const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
|
41
|
+
|
42
|
+
;(BigInt.prototype as any).toJSON = function () {
|
43
|
+
return this.toString()
|
44
|
+
}
|
45
|
+
|
46
|
+
export class ProcessorServiceImpl implements ProcessorServiceImplementation {
|
47
|
+
private started = false
|
48
|
+
// When there is unhandled error, stop process and return unavailable error
|
49
|
+
unhandled: Error
|
50
|
+
// private processorConfig: ProcessConfigResponse
|
51
|
+
|
52
|
+
private readonly loader: () => Promise<any>
|
53
|
+
|
54
|
+
private readonly shutdownHandler?: () => void
|
55
|
+
|
56
|
+
private readonly enablePreprocess: boolean
|
57
|
+
|
58
|
+
private preparedData: PreparedData | undefined
|
59
|
+
|
60
|
+
constructor(loader: () => Promise<any>, shutdownHandler?: () => void) {
|
61
|
+
this.loader = loader
|
62
|
+
this.shutdownHandler = shutdownHandler
|
63
|
+
|
64
|
+
this.enablePreprocess = process.env['ENABLE_PREPROCESS']
|
65
|
+
? process.env['ENABLE_PREPROCESS'].toLowerCase() == 'true'
|
66
|
+
: false
|
67
|
+
}
|
68
|
+
|
69
|
+
async getConfig(request: ProcessConfigRequest, context: CallContext): Promise<ProcessConfigResponse> {
|
70
|
+
if (!this.started) {
|
71
|
+
throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
|
72
|
+
}
|
73
|
+
// if (!this.processorConfig) {
|
74
|
+
// throw new ServerError(Status.INTERNAL, 'Process config empty.')
|
75
|
+
// }
|
76
|
+
|
77
|
+
// Don't use .create to keep compatiblity
|
78
|
+
const newConfig = ProcessConfigResponse.fromPartial({})
|
79
|
+
await PluginManager.INSTANCE.configure(newConfig)
|
80
|
+
return newConfig
|
81
|
+
}
|
82
|
+
|
83
|
+
//
|
84
|
+
// async configure() {
|
85
|
+
// this.processorConfig = ProcessConfigResponse.fromPartial({})
|
86
|
+
// await PluginManager.INSTANCE.configure(this.processorConfig)
|
87
|
+
// }
|
88
|
+
|
89
|
+
async start(request: StartRequest, context: CallContext): Promise<Empty> {
|
90
|
+
if (this.started) {
|
91
|
+
return {}
|
92
|
+
}
|
93
|
+
|
94
|
+
freezeGlobalConfig()
|
95
|
+
|
96
|
+
try {
|
97
|
+
// for (const plugin of ['@sentio/sdk', '@sentio/sdk/eth']) {
|
98
|
+
// try {
|
99
|
+
// await import(plugin)
|
100
|
+
// } catch (e) {
|
101
|
+
// console.error('Failed to load plugin: ', plugin)
|
102
|
+
// }
|
103
|
+
// }
|
104
|
+
//
|
105
|
+
// for (const plugin of ['@sentio/sdk/aptos', '@sentio/sdk/solana']) {
|
106
|
+
// try {
|
107
|
+
// await import(plugin)
|
108
|
+
// } catch (e) {}
|
109
|
+
// }
|
110
|
+
|
111
|
+
await this.loader()
|
112
|
+
} catch (e) {
|
113
|
+
throw new ServerError(Status.INVALID_ARGUMENT, 'Failed to load processor: ' + errorString(e))
|
114
|
+
}
|
115
|
+
|
116
|
+
await PluginManager.INSTANCE.start(request)
|
117
|
+
|
118
|
+
// try {
|
119
|
+
// await this.configure()
|
120
|
+
// } catch (e) {
|
121
|
+
// throw new ServerError(Status.INTERNAL, 'Failed to start processor : ' + errorString(e))
|
122
|
+
// }
|
123
|
+
this.started = true
|
124
|
+
return {}
|
125
|
+
}
|
126
|
+
|
127
|
+
async stop(request: Empty, context: CallContext): Promise<Empty> {
|
128
|
+
console.log('Server Shutting down in 5 seconds')
|
129
|
+
if (this.shutdownHandler) {
|
130
|
+
setTimeout(this.shutdownHandler, 5000)
|
131
|
+
}
|
132
|
+
return {}
|
133
|
+
}
|
134
|
+
|
135
|
+
async processBindings(request: ProcessBindingsRequest, options?: CallContext): Promise<ProcessBindingResponse> {
|
136
|
+
const preparedData = this.enablePreprocess
|
137
|
+
? await this.preprocessBindings(request.bindings, {}, undefined, options)
|
138
|
+
: { ethCallResults: {} }
|
139
|
+
|
140
|
+
const promises = []
|
141
|
+
for (const binding of request.bindings) {
|
142
|
+
const promise = this.processBinding(binding, preparedData)
|
143
|
+
if (GLOBAL_CONFIG.execution.sequential) {
|
144
|
+
await promise
|
145
|
+
}
|
146
|
+
promises.push(promise)
|
147
|
+
}
|
148
|
+
let promise
|
149
|
+
try {
|
150
|
+
promise = await Promise.all(promises)
|
151
|
+
processMetrics.process_binding_count.add(request.bindings.length)
|
152
|
+
} catch (e) {
|
153
|
+
processMetrics.process_binding_error.add(request.bindings.length)
|
154
|
+
throw e
|
155
|
+
}
|
156
|
+
const result = mergeProcessResults(promise)
|
157
|
+
|
158
|
+
// let updated = false
|
159
|
+
// if (PluginManager.INSTANCE.stateDiff(this.processorConfig)) {
|
160
|
+
// await this.configure()
|
161
|
+
// updated = true
|
162
|
+
// }
|
163
|
+
|
164
|
+
return {
|
165
|
+
result
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
169
|
+
async preprocessBindings(
|
170
|
+
bindings: DataBinding[],
|
171
|
+
preprocessStore: { [k: string]: any },
|
172
|
+
dbContext?: StoreContext,
|
173
|
+
options?: CallContext
|
174
|
+
): Promise<PreparedData> {
|
175
|
+
// console.debug(`preprocessBindings start, bindings: ${bindings.length}`)
|
176
|
+
const promises = []
|
177
|
+
for (const binding of bindings) {
|
178
|
+
promises.push(this.preprocessBinding(binding, preprocessStore, dbContext, options))
|
179
|
+
}
|
180
|
+
let preprocessResults: PreprocessResult[]
|
181
|
+
try {
|
182
|
+
preprocessResults = await Promise.all(promises)
|
183
|
+
} catch (e) {
|
184
|
+
throw e
|
185
|
+
}
|
186
|
+
const groupedRequests = new Map<string, EthCallParam[]>()
|
187
|
+
const providers = new Map<string, Provider>()
|
188
|
+
for (const result of preprocessResults) {
|
189
|
+
for (const param of result.ethCallParams) {
|
190
|
+
const { chainId, blockTag } = param.context!
|
191
|
+
if (!providers.has(chainId)) {
|
192
|
+
providers.set(chainId, getProvider(chainId as EthChainId))
|
193
|
+
}
|
194
|
+
const key = [chainId, blockTag].join('|')
|
195
|
+
if (!groupedRequests.has(key)) {
|
196
|
+
groupedRequests.set(key, [])
|
197
|
+
}
|
198
|
+
groupedRequests.get(key)!.push(param)
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
const start = Date.now()
|
203
|
+
const MULTICALL_THRESHOLD = 1
|
204
|
+
const callPromises: Promise<[string, string]>[] = []
|
205
|
+
const multicallPromises: Promise<[string, string][]>[] = []
|
206
|
+
|
207
|
+
for (const params of groupedRequests.values()) {
|
208
|
+
const { chainId, blockTag } = params[0].context!
|
209
|
+
const multicallAddress = getMulticallAddress(chainId as EthChainId)
|
210
|
+
if (params.length <= MULTICALL_THRESHOLD || !multicallAddress) {
|
211
|
+
for (const param of params) {
|
212
|
+
callPromises.push(
|
213
|
+
providers
|
214
|
+
.get(chainId)!
|
215
|
+
.call({
|
216
|
+
to: param.context!.address,
|
217
|
+
data: param.calldata,
|
218
|
+
blockTag
|
219
|
+
})
|
220
|
+
.then((result) => [makeEthCallKey(param), result])
|
221
|
+
)
|
222
|
+
}
|
223
|
+
continue
|
224
|
+
}
|
225
|
+
|
226
|
+
// construct multicalls
|
227
|
+
const CHUNK_SIZE = 128
|
228
|
+
for (let i = 0; i < params.length; i += CHUNK_SIZE) {
|
229
|
+
const chunk = params.slice(i, i + CHUNK_SIZE)
|
230
|
+
const calls: Multicall3Call[] = chunk.map((param) => ({
|
231
|
+
target: param.context!.address,
|
232
|
+
callData: param.calldata
|
233
|
+
}))
|
234
|
+
const data = encodeMulticallData(calls)
|
235
|
+
multicallPromises.push(
|
236
|
+
providers
|
237
|
+
.get(chainId)!
|
238
|
+
.call({
|
239
|
+
to: multicallAddress,
|
240
|
+
data: data,
|
241
|
+
blockTag
|
242
|
+
})
|
243
|
+
.then((raw) => {
|
244
|
+
const result = decodeMulticallResult(raw).returnData
|
245
|
+
if (result.length != chunk.length) {
|
246
|
+
throw new Error(`multicall result length mismatch, params: ${chunk.length}, result: ${result.length}`)
|
247
|
+
}
|
248
|
+
const ret: [string, string][] = []
|
249
|
+
for (let i = 0; i < chunk.length; i++) {
|
250
|
+
ret.push([makeEthCallKey(chunk[i]), result[i]])
|
251
|
+
}
|
252
|
+
return ret
|
253
|
+
})
|
254
|
+
)
|
255
|
+
}
|
256
|
+
}
|
257
|
+
|
258
|
+
let results: { [p: string]: string } = {}
|
259
|
+
try {
|
260
|
+
results = Object.fromEntries(await Promise.all(callPromises))
|
261
|
+
for (const multicallResult of await Promise.all(multicallPromises)) {
|
262
|
+
results = {
|
263
|
+
...results,
|
264
|
+
...Object.fromEntries(multicallResult)
|
265
|
+
}
|
266
|
+
}
|
267
|
+
} catch (e) {
|
268
|
+
console.error(`eth call error: ${e}`)
|
269
|
+
}
|
270
|
+
// console.debug(
|
271
|
+
// `${Object.keys(results).length} calls finished, actual calls: ${callPromises.length + multicallPromises.length}, elapsed: ${Date.now() - start}ms`
|
272
|
+
// )
|
273
|
+
return {
|
274
|
+
ethCallResults: results
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
async preprocessBinding(
|
279
|
+
request: DataBinding,
|
280
|
+
preprocessStore: { [k: string]: any },
|
281
|
+
dbContext?: StoreContext,
|
282
|
+
options?: CallContext
|
283
|
+
): Promise<PreprocessResult> {
|
284
|
+
if (!this.started) {
|
285
|
+
throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
|
286
|
+
}
|
287
|
+
if (this.unhandled) {
|
288
|
+
throw new RichServerError(
|
289
|
+
Status.UNAVAILABLE,
|
290
|
+
'Unhandled exception/rejection in previous request: ' + errorString(this.unhandled),
|
291
|
+
[
|
292
|
+
DebugInfo.fromPartial({
|
293
|
+
detail: this.unhandled.message,
|
294
|
+
stackEntries: this.unhandled.stack?.split('\n')
|
295
|
+
})
|
296
|
+
]
|
297
|
+
)
|
298
|
+
}
|
299
|
+
return await PluginManager.INSTANCE.preprocessBinding(request, preprocessStore, dbContext)
|
300
|
+
}
|
301
|
+
|
302
|
+
async processBinding(
|
303
|
+
request: DataBinding,
|
304
|
+
preparedData: PreparedData | undefined,
|
305
|
+
options?: CallContext
|
306
|
+
): Promise<ProcessResult> {
|
307
|
+
if (!this.started) {
|
308
|
+
throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
|
309
|
+
}
|
310
|
+
if (this.unhandled) {
|
311
|
+
throw new RichServerError(
|
312
|
+
Status.UNAVAILABLE,
|
313
|
+
'Unhandled exception/rejection in previous request: ' + errorString(this.unhandled),
|
314
|
+
[
|
315
|
+
DebugInfo.fromPartial({
|
316
|
+
detail: this.unhandled.message,
|
317
|
+
stackEntries: this.unhandled.stack?.split('\n')
|
318
|
+
})
|
319
|
+
]
|
320
|
+
)
|
321
|
+
}
|
322
|
+
|
323
|
+
const result = await PluginManager.INSTANCE.processBinding(
|
324
|
+
request,
|
325
|
+
preparedData,
|
326
|
+
PluginManager.INSTANCE.dbContextLocalStorage.getStore()
|
327
|
+
)
|
328
|
+
recordRuntimeInfo(result, request.handlerType)
|
329
|
+
return result
|
330
|
+
}
|
331
|
+
|
332
|
+
async *processBindingsStream(requests: AsyncIterable<ProcessStreamRequest>, context: CallContext) {
|
333
|
+
if (!this.started) {
|
334
|
+
throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
|
335
|
+
}
|
336
|
+
|
337
|
+
const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
|
338
|
+
this.handleRequests(requests, subject)
|
339
|
+
.then(() => {
|
340
|
+
if (this.preparedData) {
|
341
|
+
this.preparedData = { ethCallResults: {} }
|
342
|
+
}
|
343
|
+
subject.complete()
|
344
|
+
})
|
345
|
+
.catch((e) => {
|
346
|
+
console.error(e)
|
347
|
+
subject.error(e)
|
348
|
+
})
|
349
|
+
yield* from(subject).pipe(withAbort(context.signal))
|
350
|
+
}
|
351
|
+
|
352
|
+
async handlePreprocessRequests(
|
353
|
+
requests: AsyncIterable<PreprocessStreamRequest>,
|
354
|
+
subject: Subject<DeepPartial<PreprocessStreamResponse>>
|
355
|
+
) {
|
356
|
+
const contexts = new Contexts()
|
357
|
+
const preprocessStore: { [k: string]: any } = {}
|
358
|
+
|
359
|
+
for await (const request of requests) {
|
360
|
+
try {
|
361
|
+
if (request.bindings) {
|
362
|
+
const bindings = request.bindings.bindings
|
363
|
+
const dbContext = contexts.new(request.processId, subject)
|
364
|
+
const start = Date.now()
|
365
|
+
this.preprocessBindings(bindings, preprocessStore, dbContext, undefined)
|
366
|
+
.then((preparedData) => {
|
367
|
+
// TODO maybe not proper to pass data in this way
|
368
|
+
this.preparedData = {
|
369
|
+
ethCallResults: {
|
370
|
+
...this.preparedData?.ethCallResults,
|
371
|
+
...preparedData.ethCallResults
|
372
|
+
}
|
373
|
+
}
|
374
|
+
subject.next({
|
375
|
+
processId: request.processId
|
376
|
+
})
|
377
|
+
})
|
378
|
+
.catch((e) => {
|
379
|
+
console.debug(e)
|
380
|
+
dbContext.error(request.processId, e)
|
381
|
+
})
|
382
|
+
.finally(() => {
|
383
|
+
const cost = Date.now() - start
|
384
|
+
console.debug('preprocessBinding', request.processId, ' took', cost, 'ms')
|
385
|
+
contexts.delete(request.processId)
|
386
|
+
})
|
387
|
+
}
|
388
|
+
if (request.dbResult) {
|
389
|
+
const dbContext = contexts.get(request.processId)
|
390
|
+
dbContext?.result(request.dbResult)
|
391
|
+
}
|
392
|
+
} catch (e) {
|
393
|
+
// should not happen
|
394
|
+
console.error('unexpect error during handle loop', e)
|
395
|
+
}
|
396
|
+
}
|
397
|
+
}
|
398
|
+
|
399
|
+
async *preprocessBindingsStream(requests: AsyncIterable<PreprocessStreamRequest>, context: CallContext) {
|
400
|
+
if (!this.started) {
|
401
|
+
throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
|
402
|
+
}
|
403
|
+
|
404
|
+
const subject = new Subject<DeepPartial<PreprocessStreamResponse>>()
|
405
|
+
this.handlePreprocessRequests(requests, subject)
|
406
|
+
.then(() => {
|
407
|
+
subject.complete()
|
408
|
+
})
|
409
|
+
.catch((e) => {
|
410
|
+
console.error(e)
|
411
|
+
subject.error(e)
|
412
|
+
})
|
413
|
+
yield* from(subject).pipe(withAbort(context.signal))
|
414
|
+
}
|
415
|
+
|
416
|
+
private async handleRequests(
|
417
|
+
requests: AsyncIterable<ProcessStreamRequest>,
|
418
|
+
subject: Subject<DeepPartial<ProcessStreamResponse>>
|
419
|
+
) {
|
420
|
+
const contexts = new Contexts()
|
421
|
+
for await (const request of requests) {
|
422
|
+
try {
|
423
|
+
// console.debug('received request:', request)
|
424
|
+
if (request.binding) {
|
425
|
+
process_binding_count.add(1)
|
426
|
+
|
427
|
+
// Adjust binding will make some request become invalid by setting UNKNOWN HandlerType
|
428
|
+
// for older SDK version, so we just return empty result for them here
|
429
|
+
if (request.binding.handlerType === HandlerType.UNKNOWN) {
|
430
|
+
subject.next({
|
431
|
+
processId: request.processId,
|
432
|
+
result: ProcessResult.create()
|
433
|
+
})
|
434
|
+
continue
|
435
|
+
}
|
436
|
+
|
437
|
+
const binding = request.binding
|
438
|
+
const dbContext = contexts.new(request.processId, subject)
|
439
|
+
const start = Date.now()
|
440
|
+
PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
|
441
|
+
.then(async (result) => {
|
442
|
+
// await all pending db requests
|
443
|
+
await dbContext.awaitPendings()
|
444
|
+
subject.next({
|
445
|
+
result,
|
446
|
+
processId: request.processId
|
447
|
+
})
|
448
|
+
recordRuntimeInfo(result, binding.handlerType)
|
449
|
+
})
|
450
|
+
.catch((e) => {
|
451
|
+
console.debug(e)
|
452
|
+
dbContext.error(request.processId, e)
|
453
|
+
process_binding_error.add(1)
|
454
|
+
})
|
455
|
+
.finally(() => {
|
456
|
+
const cost = Date.now() - start
|
457
|
+
process_binding_time.add(cost)
|
458
|
+
contexts.delete(request.processId)
|
459
|
+
})
|
460
|
+
}
|
461
|
+
if (request.dbResult) {
|
462
|
+
const dbContext = contexts.get(request.processId)
|
463
|
+
try {
|
464
|
+
dbContext?.result(request.dbResult)
|
465
|
+
} catch (e) {
|
466
|
+
subject.error(new Error('db result error, process should stop'))
|
467
|
+
}
|
468
|
+
}
|
469
|
+
} catch (e) {
|
470
|
+
// should not happen
|
471
|
+
console.error('unexpect error during handle loop', e)
|
472
|
+
}
|
473
|
+
}
|
474
|
+
}
|
475
|
+
}
|
476
|
+
|
477
|
+
export function recordRuntimeInfo(results: ProcessResult, handlerType: HandlerType) {
|
478
|
+
for (const list of [results.gauges, results.counters, results.events, results.exports]) {
|
479
|
+
list.forEach((e) => {
|
480
|
+
e.runtimeInfo = {
|
481
|
+
from: handlerType
|
482
|
+
}
|
483
|
+
})
|
484
|
+
}
|
485
|
+
}
|
486
|
+
|
487
|
+
class Contexts {
|
488
|
+
private contexts: Map<number, StoreContext> = new Map()
|
489
|
+
|
490
|
+
get(processId: number) {
|
491
|
+
return this.contexts.get(processId)
|
492
|
+
}
|
493
|
+
|
494
|
+
new(processId: number, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
|
495
|
+
const context = new StoreContext(subject, processId)
|
496
|
+
this.contexts.set(processId, context)
|
497
|
+
return context
|
498
|
+
}
|
499
|
+
|
500
|
+
delete(processId: number) {
|
501
|
+
const context = this.get(processId)
|
502
|
+
context?.close()
|
503
|
+
this.contexts.delete(processId)
|
504
|
+
}
|
505
|
+
}
|
package/src/state.ts
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
export class State {
|
2
|
+
stateMap = new Map<string, any>()
|
3
|
+
|
4
|
+
static INSTANCE = new State()
|
5
|
+
|
6
|
+
static reset() {
|
7
|
+
State.INSTANCE = new State()
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
export abstract class StateStorage<T> {
|
12
|
+
// TODO learn how to define single instance for all subclasses
|
13
|
+
|
14
|
+
protected constructor() {
|
15
|
+
//
|
16
|
+
}
|
17
|
+
|
18
|
+
abstract initValue(): T
|
19
|
+
|
20
|
+
key(): string {
|
21
|
+
return this.constructor.name
|
22
|
+
}
|
23
|
+
|
24
|
+
getOrRegister(): T {
|
25
|
+
let metricState: T = State.INSTANCE.stateMap.get(this.key())
|
26
|
+
if (!metricState) {
|
27
|
+
metricState = this.initValue()
|
28
|
+
State.INSTANCE.stateMap.set(this.key(), metricState)
|
29
|
+
}
|
30
|
+
return metricState
|
31
|
+
}
|
32
|
+
|
33
|
+
unregister(): T {
|
34
|
+
const value = State.INSTANCE.stateMap.get(this.key())
|
35
|
+
State.INSTANCE.stateMap.delete(this.key())
|
36
|
+
return value
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
export abstract class MapStateStorage<T> extends StateStorage<Map<string, T>> {
|
41
|
+
initValue() {
|
42
|
+
return new Map<string, T>()
|
43
|
+
}
|
44
|
+
|
45
|
+
getValue(key: string): T | undefined {
|
46
|
+
const m = this.getOrRegister()
|
47
|
+
return m.get(key)
|
48
|
+
}
|
49
|
+
|
50
|
+
getValues(): T[] {
|
51
|
+
const m = this.getOrRegister()
|
52
|
+
return Array.from(m.values())
|
53
|
+
}
|
54
|
+
|
55
|
+
getOrSetValue(key: string, value: T): T {
|
56
|
+
const m = this.getOrRegister()
|
57
|
+
const oldValue = m.get(key)
|
58
|
+
if (oldValue) {
|
59
|
+
if (oldValue !== value) {
|
60
|
+
console.warn(key, 'has been registered twice, use the previous one')
|
61
|
+
}
|
62
|
+
return oldValue
|
63
|
+
}
|
64
|
+
m.set(key, value)
|
65
|
+
return value
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
export abstract class ListStateStorage<T> extends StateStorage<T[]> {
|
70
|
+
initValue() {
|
71
|
+
return []
|
72
|
+
}
|
73
|
+
|
74
|
+
getValues(): T[] {
|
75
|
+
return this.getOrRegister()
|
76
|
+
}
|
77
|
+
|
78
|
+
addValue(value: T): T {
|
79
|
+
const m = this.getOrRegister()
|
80
|
+
m.push(value)
|
81
|
+
return value
|
82
|
+
}
|
83
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { defineConfig } from 'tsup'
|
2
|
+
|
3
|
+
export default defineConfig({
|
4
|
+
esbuildOptions: (options) => {
|
5
|
+
options.banner = {
|
6
|
+
js: `import { createRequire as createRequireShim } from 'module'; const require = createRequireShim(import.meta.url);`
|
7
|
+
}
|
8
|
+
},
|
9
|
+
entry: ['src/index.ts', 'src/processor-runner.ts'],
|
10
|
+
outDir: 'lib',
|
11
|
+
minify: process.env['BRANCH'] === 'release',
|
12
|
+
sourcemap: true,
|
13
|
+
clean: true,
|
14
|
+
dts: true,
|
15
|
+
format: 'esm'
|
16
|
+
})
|
package/src/utils.ts
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
import { EthCallParam, ProcessResult } from '@sentio/protos'
|
2
|
+
|
3
|
+
// TODO better handling this, because old proto doesn't have this
|
4
|
+
import { StateResult, ProcessResult as ProcessResultFull } from './gen/processor/protos/processor.js'
|
5
|
+
|
6
|
+
import { Required } from 'utility-types'
|
7
|
+
|
8
|
+
export function mergeProcessResults(results: ProcessResult[]): Required<ProcessResult, 'states'> {
|
9
|
+
const res = {
|
10
|
+
...ProcessResultFull.create(),
|
11
|
+
states: StateResult.create()
|
12
|
+
}
|
13
|
+
|
14
|
+
for (const r of results) {
|
15
|
+
res.counters = res.counters.concat(r.counters)
|
16
|
+
res.gauges = res.gauges.concat(r.gauges)
|
17
|
+
res.events = res.events.concat(r.events)
|
18
|
+
res.exports = res.exports.concat(r.exports)
|
19
|
+
res.states = {
|
20
|
+
configUpdated: res.states?.configUpdated || r.states?.configUpdated || false
|
21
|
+
}
|
22
|
+
}
|
23
|
+
return res
|
24
|
+
}
|
25
|
+
|
26
|
+
export function errorString(e: Error): string {
|
27
|
+
return e.message + '\n' + e.stack
|
28
|
+
}
|
29
|
+
|
30
|
+
export const USER_PROCESSOR = 'user_processor'
|
31
|
+
|
32
|
+
export function makeEthCallKey(param: EthCallParam) {
|
33
|
+
if (!param.context) {
|
34
|
+
throw new Error('null context for eth call')
|
35
|
+
}
|
36
|
+
const { chainId, address, blockTag } = param.context
|
37
|
+
return `${chainId}|${address}|${blockTag}|${param.calldata}`.toLowerCase()
|
38
|
+
}
|
39
|
+
|
40
|
+
export type Semver = {
|
41
|
+
semVer?: string
|
42
|
+
major: number
|
43
|
+
minor: number
|
44
|
+
patch: number
|
45
|
+
prerelease?: string
|
46
|
+
buildmetadata?: string
|
47
|
+
}
|
48
|
+
|
49
|
+
export function parseSemver(version: string): Semver {
|
50
|
+
const [semVer, major, minor, patch, prerelease, buildmetadata] =
|
51
|
+
version.match(
|
52
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
53
|
+
) ?? []
|
54
|
+
return {
|
55
|
+
semVer,
|
56
|
+
major: parseInt(major),
|
57
|
+
minor: parseInt(minor),
|
58
|
+
patch: parseInt(patch),
|
59
|
+
prerelease,
|
60
|
+
buildmetadata
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
export function compareSemver(a: Semver, b: Semver) {
|
65
|
+
const { major: ma, minor: mia, patch: pa, prerelease: pra } = a
|
66
|
+
const { major: mb, minor: mib, patch: pb, prerelease: prb } = b
|
67
|
+
|
68
|
+
if (ma !== mb) {
|
69
|
+
return ma - mb
|
70
|
+
}
|
71
|
+
if (mia !== mib) {
|
72
|
+
return mia - mib
|
73
|
+
}
|
74
|
+
|
75
|
+
if (pa !== pb) {
|
76
|
+
return pa - pb
|
77
|
+
}
|
78
|
+
if (pra && prb) {
|
79
|
+
const [sa, va] = pra.split('.')
|
80
|
+
const [sb, vb] = prb.split('.')
|
81
|
+
|
82
|
+
if (sa !== sb) {
|
83
|
+
return sa.localeCompare(sb)
|
84
|
+
}
|
85
|
+
|
86
|
+
return parseInt(va) - parseInt(vb)
|
87
|
+
} else if (pra) {
|
88
|
+
return -1
|
89
|
+
} else if (prb) {
|
90
|
+
return 1
|
91
|
+
}
|
92
|
+
return 0
|
93
|
+
}
|