envio 3.0.0-alpha.2 → 3.0.0-alpha.3

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.
Files changed (60) hide show
  1. package/evm.schema.json +44 -33
  2. package/fuel.schema.json +32 -21
  3. package/index.d.ts +1 -0
  4. package/package.json +7 -6
  5. package/src/Batch.res.mjs +1 -1
  6. package/src/Benchmark.res +394 -0
  7. package/src/Benchmark.res.mjs +398 -0
  8. package/src/ChainFetcher.res +459 -0
  9. package/src/ChainFetcher.res.mjs +281 -0
  10. package/src/ChainManager.res +179 -0
  11. package/src/ChainManager.res.mjs +139 -0
  12. package/src/Config.res +15 -1
  13. package/src/Config.res.mjs +27 -4
  14. package/src/Ecosystem.res +9 -124
  15. package/src/Ecosystem.res.mjs +19 -160
  16. package/src/Env.res +0 -1
  17. package/src/Env.res.mjs +0 -3
  18. package/src/Envio.gen.ts +9 -1
  19. package/src/Envio.res +12 -9
  20. package/src/EventProcessing.res +476 -0
  21. package/src/EventProcessing.res.mjs +341 -0
  22. package/src/FetchState.res +54 -29
  23. package/src/FetchState.res.mjs +62 -35
  24. package/src/GlobalState.res +1169 -0
  25. package/src/GlobalState.res.mjs +1196 -0
  26. package/src/Internal.res +2 -1
  27. package/src/LoadLayer.res +444 -0
  28. package/src/LoadLayer.res.mjs +296 -0
  29. package/src/LoadLayer.resi +32 -0
  30. package/src/Prometheus.res +8 -8
  31. package/src/Prometheus.res.mjs +10 -10
  32. package/src/ReorgDetection.res +6 -10
  33. package/src/ReorgDetection.res.mjs +6 -6
  34. package/src/UserContext.res +356 -0
  35. package/src/UserContext.res.mjs +238 -0
  36. package/src/bindings/DateFns.res +71 -0
  37. package/src/bindings/DateFns.res.mjs +22 -0
  38. package/src/sources/Evm.res +87 -0
  39. package/src/sources/Evm.res.mjs +105 -0
  40. package/src/sources/EvmChain.res +95 -0
  41. package/src/sources/EvmChain.res.mjs +61 -0
  42. package/src/sources/Fuel.res +19 -34
  43. package/src/sources/Fuel.res.mjs +34 -16
  44. package/src/sources/FuelSDK.res +37 -0
  45. package/src/sources/FuelSDK.res.mjs +29 -0
  46. package/src/sources/HyperFuel.res +2 -2
  47. package/src/sources/HyperFuel.resi +1 -1
  48. package/src/sources/HyperFuelClient.res +2 -2
  49. package/src/sources/HyperFuelSource.res +8 -8
  50. package/src/sources/HyperFuelSource.res.mjs +5 -5
  51. package/src/sources/HyperSyncSource.res +5 -5
  52. package/src/sources/HyperSyncSource.res.mjs +5 -5
  53. package/src/sources/RpcSource.res +4 -4
  54. package/src/sources/RpcSource.res.mjs +3 -3
  55. package/src/sources/Solana.res +59 -0
  56. package/src/sources/Solana.res.mjs +79 -0
  57. package/src/sources/Source.res +2 -2
  58. package/src/sources/SourceManager.res +24 -32
  59. package/src/sources/SourceManager.res.mjs +20 -20
  60. package/src/sources/SourceManager.resi +4 -5
package/src/Internal.res CHANGED
@@ -169,7 +169,8 @@ type eventItem = private {
169
169
  type blockEvent
170
170
 
171
171
  type onBlockArgs = {
172
- block: blockEvent,
172
+ slot?: int,
173
+ block?: blockEvent,
173
174
  context: handlerContext,
174
175
  }
175
176
 
@@ -0,0 +1,444 @@
1
+ open Belt
2
+
3
+ let loadById = (
4
+ ~loadManager,
5
+ ~persistence: Persistence.t,
6
+ ~entityConfig: Internal.entityConfig,
7
+ ~inMemoryStore,
8
+ ~shouldGroup,
9
+ ~item,
10
+ ~entityId,
11
+ ) => {
12
+ let key = `${entityConfig.name}.get`
13
+ let inMemTable = inMemoryStore->InMemoryStore.getInMemTable(~entityConfig)
14
+
15
+ let load = async (idsToLoad, ~onError as _) => {
16
+ let timerRef = Prometheus.StorageLoad.startOperation(~operation=key)
17
+
18
+ // Since LoadManager.call prevents registerign entities already existing in the inMemoryStore,
19
+ // we can be sure that we load only the new ones.
20
+ let dbEntities = try {
21
+ await (persistence->Persistence.getInitializedStorageOrThrow).loadByIdsOrThrow(
22
+ ~table=entityConfig.table,
23
+ ~rowsSchema=entityConfig.rowsSchema,
24
+ ~ids=idsToLoad,
25
+ )
26
+ } catch {
27
+ | Persistence.StorageError({message, reason}) =>
28
+ reason->ErrorHandling.mkLogAndRaise(~logger=item->Logging.getItemLogger, ~msg=message)
29
+ }
30
+
31
+ let entitiesMap = Js.Dict.empty()
32
+ for idx in 0 to dbEntities->Array.length - 1 {
33
+ let entity = dbEntities->Js.Array2.unsafe_get(idx)
34
+ entitiesMap->Js.Dict.set(entity.id, entity)
35
+ }
36
+ idsToLoad->Js.Array2.forEach(entityId => {
37
+ // Set the entity in the in memory store
38
+ // without overwriting existing values
39
+ // which might be newer than what we got from db
40
+ inMemTable->InMemoryTable.Entity.initValue(
41
+ ~allowOverWriteEntity=false,
42
+ ~key=entityId,
43
+ ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
44
+ )
45
+ })
46
+
47
+ timerRef->Prometheus.StorageLoad.endOperation(
48
+ ~operation=key,
49
+ ~whereSize=idsToLoad->Array.length,
50
+ ~size=dbEntities->Array.length,
51
+ )
52
+ }
53
+
54
+ loadManager->LoadManager.call(
55
+ ~key,
56
+ ~load,
57
+ ~shouldGroup,
58
+ ~hasher=LoadManager.noopHasher,
59
+ ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafe,
60
+ ~hasInMemory=hash => inMemTable.table->InMemoryTable.hasByHash(hash),
61
+ ~input=entityId,
62
+ )
63
+ }
64
+
65
+ let callEffect = (
66
+ ~effect: Internal.effect,
67
+ ~arg: Internal.effectArgs,
68
+ ~inMemTable: InMemoryStore.effectCacheInMemTable,
69
+ ~timerRef,
70
+ ~onError,
71
+ ) => {
72
+ let effectName = effect.name
73
+ let hadActiveCalls = effect.activeCallsCount > 0
74
+ effect.activeCallsCount = effect.activeCallsCount + 1
75
+ Prometheus.EffectCalls.activeCallsCount->Prometheus.SafeGauge.handleInt(
76
+ ~labels=effectName,
77
+ ~value=effect.activeCallsCount,
78
+ )
79
+
80
+ if hadActiveCalls {
81
+ let elapsed = Hrtime.millisBetween(~from=effect.prevCallStartTimerRef, ~to=timerRef)
82
+ if elapsed > 0 {
83
+ Prometheus.EffectCalls.timeCounter->Prometheus.SafeCounter.incrementMany(
84
+ ~labels=effectName,
85
+ ~value=Hrtime.millisBetween(~from=effect.prevCallStartTimerRef, ~to=timerRef),
86
+ )
87
+ }
88
+ }
89
+ effect.prevCallStartTimerRef = timerRef
90
+
91
+ effect.handler(arg)
92
+ ->Promise.thenResolve(output => {
93
+ inMemTable.dict->Js.Dict.set(arg.cacheKey, output)
94
+ if arg.context.cache {
95
+ inMemTable.idsToStore->Array.push(arg.cacheKey)->ignore
96
+ }
97
+ })
98
+ ->Promise.catchResolve(exn => {
99
+ onError(~inputKey=arg.cacheKey, ~exn)
100
+ })
101
+ ->Promise.finally(() => {
102
+ effect.activeCallsCount = effect.activeCallsCount - 1
103
+ Prometheus.EffectCalls.activeCallsCount->Prometheus.SafeGauge.handleInt(
104
+ ~labels=effectName,
105
+ ~value=effect.activeCallsCount,
106
+ )
107
+ let newTimer = Hrtime.makeTimer()
108
+ Prometheus.EffectCalls.timeCounter->Prometheus.SafeCounter.incrementMany(
109
+ ~labels=effectName,
110
+ ~value=Hrtime.millisBetween(~from=effect.prevCallStartTimerRef, ~to=newTimer),
111
+ )
112
+ effect.prevCallStartTimerRef = newTimer
113
+
114
+ Prometheus.EffectCalls.totalCallsCount->Prometheus.SafeCounter.increment(~labels=effectName)
115
+ Prometheus.EffectCalls.sumTimeCounter->Prometheus.SafeCounter.incrementMany(
116
+ ~labels=effectName,
117
+ ~value=timerRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis,
118
+ )
119
+ })
120
+ }
121
+
122
+ let rec executeWithRateLimit = (
123
+ ~effect: Internal.effect,
124
+ ~effectArgs: array<Internal.effectArgs>,
125
+ ~inMemTable,
126
+ ~onError,
127
+ ~isFromQueue: bool,
128
+ ) => {
129
+ let effectName = effect.name
130
+
131
+ let timerRef = Hrtime.makeTimer()
132
+ let promises = []
133
+
134
+ switch effect.rateLimit {
135
+ | None =>
136
+ // No rate limiting - execute all immediately
137
+ for idx in 0 to effectArgs->Array.length - 1 {
138
+ promises
139
+ ->Array.push(
140
+ callEffect(
141
+ ~effect,
142
+ ~arg=effectArgs->Array.getUnsafe(idx),
143
+ ~inMemTable,
144
+ ~timerRef,
145
+ ~onError,
146
+ )->Promise.ignoreValue,
147
+ )
148
+ ->ignore
149
+ }
150
+
151
+ | Some(state) =>
152
+ let now = Js.Date.now()
153
+
154
+ // Check if we need to reset the window
155
+ if now >= state.windowStartTime +. state.durationMs->Int.toFloat {
156
+ state.availableCalls = state.callsPerDuration
157
+ state.windowStartTime = now
158
+ state.nextWindowPromise = None
159
+ }
160
+
161
+ // Split into immediate and queued
162
+ let immediateCount = Js.Math.min_int(state.availableCalls, effectArgs->Array.length)
163
+ let immediateArgs = effectArgs->Array.slice(~offset=0, ~len=immediateCount)
164
+ let queuedArgs = effectArgs->Array.sliceToEnd(immediateCount)
165
+
166
+ // Update available calls
167
+ state.availableCalls = state.availableCalls - immediateCount
168
+
169
+ // Call immediate effects
170
+ for idx in 0 to immediateArgs->Array.length - 1 {
171
+ promises
172
+ ->Array.push(
173
+ callEffect(
174
+ ~effect,
175
+ ~arg=immediateArgs->Array.getUnsafe(idx),
176
+ ~inMemTable,
177
+ ~timerRef,
178
+ ~onError,
179
+ )->Promise.ignoreValue,
180
+ )
181
+ ->ignore
182
+ }
183
+
184
+ if immediateCount > 0 && isFromQueue {
185
+ // Update queue count metric
186
+ state.queueCount = state.queueCount - immediateCount
187
+ Prometheus.EffectQueueCount.set(~count=state.queueCount, ~effectName)
188
+ }
189
+
190
+ // Handle queued items
191
+ if queuedArgs->Utils.Array.notEmpty {
192
+ if !isFromQueue {
193
+ // Update queue count metric
194
+ state.queueCount = state.queueCount + queuedArgs->Array.length
195
+ Prometheus.EffectQueueCount.set(~count=state.queueCount, ~effectName)
196
+ }
197
+
198
+ let millisUntilReset = ref(0)
199
+ let nextWindowPromise = switch state.nextWindowPromise {
200
+ | Some(p) => p
201
+ | None =>
202
+ millisUntilReset :=
203
+ (state.windowStartTime +. state.durationMs->Int.toFloat -. now)->Float.toInt
204
+ let p = Utils.delay(millisUntilReset.contents)
205
+ state.nextWindowPromise = Some(p)
206
+ p
207
+ }
208
+
209
+ // Wait for next window and recursively process queue
210
+ promises
211
+ ->Array.push(
212
+ nextWindowPromise
213
+ ->Promise.then(() => {
214
+ if millisUntilReset.contents > 0 {
215
+ Prometheus.EffectQueueCount.timeCounter->Prometheus.SafeCounter.incrementMany(
216
+ ~labels=effectName,
217
+ ~value=millisUntilReset.contents,
218
+ )
219
+ }
220
+ executeWithRateLimit(
221
+ ~effect,
222
+ ~effectArgs=queuedArgs,
223
+ ~inMemTable,
224
+ ~onError,
225
+ ~isFromQueue=true,
226
+ )
227
+ })
228
+ ->Promise.ignoreValue,
229
+ )
230
+ ->ignore
231
+ }
232
+ }
233
+
234
+ // Wait for all to complete
235
+ promises->Promise.all
236
+ }
237
+
238
+ let loadEffect = (
239
+ ~loadManager,
240
+ ~persistence: Persistence.t,
241
+ ~effect: Internal.effect,
242
+ ~effectArgs,
243
+ ~inMemoryStore,
244
+ ~shouldGroup,
245
+ ~item,
246
+ ) => {
247
+ let effectName = effect.name
248
+ let key = `${effectName}.effect`
249
+ let inMemTable = inMemoryStore->InMemoryStore.getEffectInMemTable(~effect)
250
+
251
+ let load = async (args, ~onError) => {
252
+ let idsToLoad = args->Js.Array2.map((arg: Internal.effectArgs) => arg.cacheKey)
253
+ let idsFromCache = Utils.Set.make()
254
+
255
+ if (
256
+ switch persistence.storageStatus {
257
+ | Ready({cache}) => cache->Utils.Dict.has(effectName)
258
+ | _ => false
259
+ }
260
+ ) {
261
+ let timerRef = Prometheus.StorageLoad.startOperation(~operation=key)
262
+ let {table, outputSchema} = effect.storageMeta
263
+
264
+ let dbEntities = try {
265
+ await (persistence->Persistence.getInitializedStorageOrThrow).loadByIdsOrThrow(
266
+ ~table,
267
+ ~rowsSchema=Internal.effectCacheItemRowsSchema,
268
+ ~ids=idsToLoad,
269
+ )
270
+ } catch {
271
+ | exn =>
272
+ item
273
+ ->Logging.getItemLogger
274
+ ->Logging.childWarn({
275
+ "msg": `Failed to load cache effect cache. The indexer will continue working, but the effect will not be able to use the cache.`,
276
+ "err": exn->Utils.prettifyExn,
277
+ "effect": effectName,
278
+ })
279
+ []
280
+ }
281
+
282
+ dbEntities->Js.Array2.forEach(dbEntity => {
283
+ try {
284
+ let output = dbEntity.output->S.parseOrThrow(outputSchema)
285
+ idsFromCache->Utils.Set.add(dbEntity.id)->ignore
286
+ inMemTable.dict->Js.Dict.set(dbEntity.id, output)
287
+ } catch {
288
+ | S.Raised(error) =>
289
+ inMemTable.invalidationsCount = inMemTable.invalidationsCount + 1
290
+ Prometheus.EffectCacheInvalidationsCount.increment(~effectName)
291
+ item
292
+ ->Logging.getItemLogger
293
+ ->Logging.childTrace({
294
+ "msg": "Invalidated effect cache",
295
+ "input": dbEntity.id,
296
+ "effect": effectName,
297
+ "err": error->S.Error.message,
298
+ })
299
+ }
300
+ })
301
+
302
+ timerRef->Prometheus.StorageLoad.endOperation(
303
+ ~operation=key,
304
+ ~whereSize=idsToLoad->Array.length,
305
+ ~size=dbEntities->Array.length,
306
+ )
307
+ }
308
+
309
+ let remainingCallsCount = idsToLoad->Array.length - idsFromCache->Utils.Set.size
310
+ if remainingCallsCount > 0 {
311
+ let argsToCall = []
312
+ for idx in 0 to args->Array.length - 1 {
313
+ let arg = args->Array.getUnsafe(idx)
314
+ if !(idsFromCache->Utils.Set.has(arg.cacheKey)) {
315
+ argsToCall->Array.push(arg)->ignore
316
+ }
317
+ }
318
+
319
+ if argsToCall->Utils.Array.notEmpty {
320
+ await executeWithRateLimit(
321
+ ~effect,
322
+ ~effectArgs=argsToCall,
323
+ ~inMemTable,
324
+ ~onError,
325
+ ~isFromQueue=false,
326
+ )->Promise.ignoreValue
327
+ }
328
+ }
329
+ }
330
+
331
+ loadManager->LoadManager.call(
332
+ ~key,
333
+ ~load,
334
+ ~shouldGroup,
335
+ ~hasher=args => args.cacheKey,
336
+ ~getUnsafeInMemory=hash => inMemTable.dict->Js.Dict.unsafeGet(hash),
337
+ ~hasInMemory=hash => inMemTable.dict->Utils.Dict.has(hash),
338
+ ~input=effectArgs,
339
+ )
340
+ }
341
+
342
+ let loadByField = (
343
+ ~loadManager,
344
+ ~persistence: Persistence.t,
345
+ ~operator: TableIndices.Operator.t,
346
+ ~entityConfig: Internal.entityConfig,
347
+ ~inMemoryStore,
348
+ ~fieldName,
349
+ ~fieldValueSchema,
350
+ ~shouldGroup,
351
+ ~item,
352
+ ~fieldValue,
353
+ ) => {
354
+ let operatorCallName = switch operator {
355
+ | Eq => "eq"
356
+ | Gt => "gt"
357
+ | Lt => "lt"
358
+ }
359
+ let key = `${entityConfig.name}.getWhere.${fieldName}.${operatorCallName}`
360
+ let inMemTable = inMemoryStore->InMemoryStore.getInMemTable(~entityConfig)
361
+
362
+ let load = async (fieldValues: array<'fieldValue>, ~onError as _) => {
363
+ let timerRef = Prometheus.StorageLoad.startOperation(~operation=key)
364
+
365
+ let size = ref(0)
366
+
367
+ let indiciesToLoad = fieldValues->Js.Array2.map((fieldValue): TableIndices.Index.t => {
368
+ Single({
369
+ fieldName,
370
+ fieldValue: TableIndices.FieldValue.castFrom(fieldValue),
371
+ operator,
372
+ })
373
+ })
374
+
375
+ let _ =
376
+ await indiciesToLoad
377
+ ->Js.Array2.map(async index => {
378
+ inMemTable->InMemoryTable.Entity.addEmptyIndex(~index)
379
+ try {
380
+ let entities = await (
381
+ persistence->Persistence.getInitializedStorageOrThrow
382
+ ).loadByFieldOrThrow(
383
+ ~operator=switch index {
384
+ | Single({operator: Gt}) => #">"
385
+ | Single({operator: Eq}) => #"="
386
+ | Single({operator: Lt}) => #"<"
387
+ },
388
+ ~table=entityConfig.table,
389
+ ~rowsSchema=entityConfig.rowsSchema,
390
+ ~fieldName=index->TableIndices.Index.getFieldName,
391
+ ~fieldValue=switch index {
392
+ | Single({fieldValue}) => fieldValue
393
+ },
394
+ ~fieldSchema=fieldValueSchema->(
395
+ Utils.magic: S.t<'fieldValue> => S.t<TableIndices.FieldValue.t>
396
+ ),
397
+ )
398
+
399
+ entities->Array.forEach(entity => {
400
+ //Set the entity in the in memory store
401
+ inMemTable->InMemoryTable.Entity.initValue(
402
+ ~allowOverWriteEntity=false,
403
+ ~key=entity.id,
404
+ ~entity=Some(entity),
405
+ )
406
+ })
407
+
408
+ size := size.contents + entities->Array.length
409
+ } catch {
410
+ | Persistence.StorageError({message, reason}) =>
411
+ reason->ErrorHandling.mkLogAndRaise(
412
+ ~logger=Logging.createChildFrom(
413
+ ~logger=item->Logging.getItemLogger,
414
+ ~params={
415
+ "operator": operatorCallName,
416
+ "tableName": entityConfig.table.tableName,
417
+ "fieldName": fieldName,
418
+ "fieldValue": fieldValue,
419
+ },
420
+ ),
421
+ ~msg=message,
422
+ )
423
+ }
424
+ })
425
+ ->Promise.all
426
+
427
+ timerRef->Prometheus.StorageLoad.endOperation(
428
+ ~operation=key,
429
+ ~whereSize=fieldValues->Array.length,
430
+ ~size=size.contents,
431
+ )
432
+ }
433
+
434
+ loadManager->LoadManager.call(
435
+ ~key,
436
+ ~load,
437
+ ~input=fieldValue,
438
+ ~shouldGroup,
439
+ ~hasher=fieldValue =>
440
+ fieldValue->TableIndices.FieldValue.castFrom->TableIndices.FieldValue.toString,
441
+ ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafeOnIndex(~fieldName, ~operator),
442
+ ~hasInMemory=inMemTable->InMemoryTable.Entity.hasIndex(~fieldName, ~operator),
443
+ )
444
+ }