envio 3.11.0 → 3.12.1

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 (109) hide show
  1. package/index.d.ts +3 -0
  2. package/package.json +6 -6
  3. package/src/Batch.res +111 -78
  4. package/src/Batch.res.mjs +53 -46
  5. package/src/BatchProcessing.res +1 -11
  6. package/src/BatchProcessing.res.mjs +1 -4
  7. package/src/ChainFetching.res +7 -3
  8. package/src/ChainFetching.res.mjs +1 -1
  9. package/src/ChainState.res +82 -20
  10. package/src/ChainState.res.mjs +51 -13
  11. package/src/ChainState.resi +6 -1
  12. package/src/Config.res +58 -37
  13. package/src/Config.res.mjs +63 -53
  14. package/src/CrossChainState.res +50 -53
  15. package/src/CrossChainState.res.mjs +25 -23
  16. package/src/CrossChainState.resi +6 -13
  17. package/src/EffectState.res +13 -4
  18. package/src/EffectState.resi +7 -1
  19. package/src/EventProcessing.res +6 -6
  20. package/src/EventProcessing.res.mjs +6 -6
  21. package/src/FetchState.res +65 -46
  22. package/src/FetchState.res.mjs +68 -36
  23. package/src/FinalizeBackfill.res +16 -12
  24. package/src/FinalizeBackfill.res.mjs +3 -3
  25. package/src/HistoryPolicy.res +46 -0
  26. package/src/HistoryPolicy.res.mjs +50 -0
  27. package/src/InMemoryStore.res +30 -32
  28. package/src/InMemoryStore.res.mjs +33 -28
  29. package/src/InMemoryTable.res +155 -67
  30. package/src/InMemoryTable.res.mjs +151 -60
  31. package/src/IndexerState.res +66 -45
  32. package/src/IndexerState.res.mjs +57 -29
  33. package/src/IndexerState.resi +8 -13
  34. package/src/Internal.res +4 -2
  35. package/src/LoadLayer.res +68 -38
  36. package/src/LoadLayer.res.mjs +50 -64
  37. package/src/LoadLayer.resi +1 -1
  38. package/src/Main.res +25 -24
  39. package/src/Main.res.mjs +24 -25
  40. package/src/Metrics.res +49 -1
  41. package/src/Metrics.res.mjs +26 -3
  42. package/src/Persistence.res +55 -16
  43. package/src/Persistence.res.mjs +18 -3
  44. package/src/PgStorage.res +286 -180
  45. package/src/PgStorage.res.mjs +209 -122
  46. package/src/PruneStaleHistory.res +9 -15
  47. package/src/PruneStaleHistory.res.mjs +11 -20
  48. package/src/Rollback.res +9 -14
  49. package/src/Rollback.res.mjs +5 -3
  50. package/src/Sink.res +7 -5
  51. package/src/Sink.res.mjs +4 -4
  52. package/src/Staging.res +298 -0
  53. package/src/Staging.res.mjs +257 -0
  54. package/src/TestIndexer.res +14 -30
  55. package/src/TestIndexer.res.mjs +11 -9
  56. package/src/UserContext.res +20 -36
  57. package/src/UserContext.res.mjs +6 -9
  58. package/src/Utils.res +10 -4
  59. package/src/Utils.res.mjs +16 -16
  60. package/src/Writing.res +34 -20
  61. package/src/Writing.res.mjs +15 -20
  62. package/src/bindings/ClickHouse.res +41 -34
  63. package/src/bindings/ClickHouse.res.mjs +36 -30
  64. package/src/bindings/ClickHouseSink.res +45 -186
  65. package/src/bindings/ClickHouseSink.res.mjs +21 -174
  66. package/src/db/CheckpointSequence.res +109 -0
  67. package/src/db/CheckpointSequence.res.mjs +99 -0
  68. package/src/db/EntityFilter.res +487 -275
  69. package/src/db/EntityFilter.res.mjs +557 -309
  70. package/src/db/EntityHistory.res +6 -5
  71. package/src/db/EntityHistory.res.mjs +5 -5
  72. package/src/db/Frontier.res +86 -0
  73. package/src/db/Frontier.res.mjs +126 -0
  74. package/src/db/InternalTable.res +145 -27
  75. package/src/db/InternalTable.res.mjs +115 -29
  76. package/src/db/RollbackFloors.res +34 -41
  77. package/src/db/RollbackFloors.res.mjs +27 -71
  78. package/src/db/Table.res +21 -6
  79. package/src/db/Table.res.mjs +13 -4
  80. package/src/sources/BlockStore.res +8 -0
  81. package/src/sources/EvmHyperSyncSource.res +5 -1
  82. package/src/sources/EvmHyperSyncSource.res.mjs +4 -3
  83. package/src/sources/FuelHyperSyncSource.res +1 -0
  84. package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
  85. package/src/sources/HyperSync.res +6 -0
  86. package/src/sources/HyperSync.res.mjs +5 -2
  87. package/src/sources/HyperSync.resi +2 -0
  88. package/src/sources/HyperSyncClient.res +6 -0
  89. package/src/sources/HyperSyncSSE.res +1 -1
  90. package/src/sources/HyperSyncSSE.res.mjs +4 -10
  91. package/src/sources/RequestStat.res +8 -1
  92. package/src/sources/RpcSource.res +1 -0
  93. package/src/sources/RpcSource.res.mjs +1 -1
  94. package/src/sources/SimulateSource.res +1 -0
  95. package/src/sources/SimulateSource.res.mjs +1 -1
  96. package/src/sources/Source.res +11 -3
  97. package/src/sources/SourceManager.res +34 -13
  98. package/src/sources/SourceManager.res.mjs +26 -9
  99. package/src/sources/SourceManager.resi +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +5 -0
  101. package/src/sources/SvmHyperSyncSource.res +3 -0
  102. package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
  103. package/src/tui/Tui.res +56 -143
  104. package/src/tui/Tui.res.mjs +70 -166
  105. package/src/tui/components/TuiData.res +61 -16
  106. package/src/tui/components/TuiData.res.mjs +53 -15
  107. package/svm.schema.json +0 -7
  108. package/src/db/CheckpointBounds.res +0 -53
  109. package/src/db/CheckpointBounds.res.mjs +0 -44
package/src/LoadLayer.res CHANGED
@@ -6,23 +6,6 @@ let scopeKeySuffix = (scope: Internal.chainScope) =>
6
6
  | Chain(chainId) => `.${chainId->ChainId.toString}`
7
7
  }
8
8
 
9
- // Narrows a query to the scope's chain. Cross-chain entities have no chain-id
10
- // column, so their filter is left untouched.
11
- let scopeFilter = (filter: EntityFilter.t, ~table: Table.table, ~scope: Internal.chainScope) =>
12
- switch (scope, table->Table.getChainIdField) {
13
- | (Chain(chainId), Some(field)) =>
14
- EntityFilter.And({
15
- filters: [
16
- filter,
17
- Eq({
18
- fieldName: field.fieldName,
19
- fieldValue: chainId->(Utils.magic: ChainId.t => unknown),
20
- }),
21
- ],
22
- })
23
- | _ => filter
24
- }
25
-
26
9
  let loadById = (
27
10
  ~loadManager,
28
11
  ~persistence: Persistence.t,
@@ -48,10 +31,10 @@ let loadById = (
48
31
  (
49
32
  await storage.loadOrThrow(
50
33
  ~table=entityConfig.table,
51
- ~filter=EntityFilter.In({
52
- fieldName: Table.idFieldName,
53
- fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
54
- })->scopeFilter(~table=entityConfig.table, ~scope),
34
+ ~filter=EntityFilter.byIds(idsToLoad)->EntityFilter.scoped(
35
+ ~table=entityConfig.table,
36
+ ~scope,
37
+ ),
55
38
  )
56
39
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
57
40
  } catch {
@@ -69,9 +52,10 @@ let loadById = (
69
52
  let entity = dbEntities->Array.getUnsafe(idx)
70
53
  entitiesMap->Dict.set(entity.id, entity)
71
54
  }
55
+ let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
72
56
  idsToLoad->Array.forEach(entityId => {
73
57
  inMemTable->InMemoryTable.Entity.initValue(
74
- ~committedCheckpointId=indexerState->IndexerState.committedCheckpointId,
58
+ ~committedCheckpointId,
75
59
  ~key=entityId,
76
60
  ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
77
61
  )
@@ -109,6 +93,7 @@ let callEffect = (
109
93
  effect.handler(arg)
110
94
  ->Promise.thenResolve(output => {
111
95
  inMemTable->InMemoryStore.setEffectOutput(
96
+ ~chainId=arg.chainId,
112
97
  ~checkpointId=arg.checkpointId,
113
98
  ~cacheKey=arg.cacheKey,
114
99
  ~output,
@@ -271,15 +256,9 @@ let loadEffect = (
271
256
  let {outputSchema} = effect.storageMeta
272
257
 
273
258
  let dbEntities = try {
274
- (
275
- await storage.loadOrThrow(
276
- ~table,
277
- ~filter=EntityFilter.In({
278
- fieldName: Table.idFieldName,
279
- fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
280
- }),
281
- )
282
- )->(Utils.magic: array<unknown> => array<Internal.effectCacheItem>)
259
+ (await storage.loadOrThrow(~table, ~filter=EntityFilter.byIds(idsToLoad)))->(
260
+ Utils.magic: array<unknown> => array<Internal.effectCacheItem>
261
+ )
283
262
  } catch {
284
263
  | exn =>
285
264
  Ecosystem.getItemLogger(item, ~ecosystem)->Logging.childWarn({
@@ -294,7 +273,11 @@ let loadEffect = (
294
273
  try {
295
274
  let output = dbEntity.output->S.parseOrThrow(outputSchema)
296
275
  idsFromCache->Utils.Set.add(dbEntity.id)->ignore
297
- inMemTable->InMemoryStore.initEffectOutputFromDb(~cacheKey=dbEntity.id, ~output)
276
+ inMemTable->InMemoryStore.initEffectOutputFromDb(
277
+ ~chainId=item->Internal.getItemChainId,
278
+ ~cacheKey=dbEntity.id,
279
+ ~output,
280
+ )
298
281
  } catch {
299
282
  | S.Raised(error) =>
300
283
  inMemTable->EffectState.recordInvalidation
@@ -349,7 +332,7 @@ let loadEffect = (
349
332
  )
350
333
  }
351
334
 
352
- let loadByFilter = (
335
+ let loadByParsedFilter = (
353
336
  ~loadManager,
354
337
  ~persistence: Persistence.t,
355
338
  ~entityConfig: Internal.entityConfig,
@@ -372,14 +355,16 @@ let loadByFilter = (
372
355
 
373
356
  let size = ref(0)
374
357
 
375
- filters->Array.forEach(filter => inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter))
358
+ filters->Array.forEach(filter =>
359
+ inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter, ~table=entityConfig.table)
360
+ )
376
361
 
377
362
  // Any non-derived field can be filtered on, so the columns this query reads
378
363
  // are indexed on demand before it runs rather than promised by the schema.
379
364
  // Inside the load timing: waiting on the build is time the handler spends
380
365
  // waiting for this operation, and it's the only thing that explains an
381
366
  // occasional very slow getWhere.
382
- await storage.ensureQueryIndexes(~table=entityConfig.table, ~filters)
367
+ await storage.ensureQueryIndexes(~entityConfig, ~scope, ~filters)
383
368
 
384
369
  // Loading a superset of rows via a merged query is safe: every loaded
385
370
  // entity is matched against all registered indexes, not only the
@@ -393,18 +378,23 @@ let loadByFilter = (
393
378
  (
394
379
  await storage.loadOrThrow(
395
380
  ~table=entityConfig.table,
396
- ~filter=filter->scopeFilter(~table=entityConfig.table, ~scope),
381
+ ~filter=filter->EntityFilter.scoped(~table=entityConfig.table, ~scope),
397
382
  )
398
383
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
399
384
 
385
+ let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
400
386
  entities->Array.forEach(entity => {
401
387
  inMemTable->InMemoryTable.Entity.initValue(
402
- ~committedCheckpointId=indexerState->IndexerState.committedCheckpointId,
388
+ ~committedCheckpointId,
403
389
  ~key=entity.id,
404
390
  ~entity=Some(entity),
405
391
  )
406
392
  })
407
393
 
394
+ // Every row this filter's values could match is now in the table, so a
395
+ // later getWhere naming any of them needs no round trip of its own.
396
+ inMemTable->InMemoryTable.Entity.recordLoadedValues(~filter, ~table=entityConfig.table)
397
+
408
398
  size := size.contents + entities->Array.length
409
399
  } catch {
410
400
  | Persistence.StorageError({message, reason}) =>
@@ -435,13 +425,53 @@ let loadByFilter = (
435
425
  )
436
426
  }
437
427
 
428
+ // Keying an _in walks every value, so it's computed once here and handed to
429
+ // the load manager rather than recomputed by the hasher.
430
+ let filterKey = filter->EntityFilter.toString(~table=entityConfig.table)
431
+
432
+ if !(inMemTable->InMemoryTable.Entity.hasIndex)(filterKey) {
433
+ inMemTable->InMemoryTable.Entity.tryIndexFromLoadedValues(~filter, ~table=entityConfig.table)
434
+ }
435
+
438
436
  loadManager->LoadManager.call(
439
437
  ~key,
440
438
  ~load,
441
439
  ~input=filter,
442
440
  ~shouldGroup,
443
- ~hasher=EntityFilter.toString,
441
+ ~hasher=_ => filterKey,
444
442
  ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafeOnIndex,
445
443
  ~hasInMemory=inMemTable->InMemoryTable.Entity.hasIndex,
446
444
  )
447
445
  }
446
+
447
+ let loadByFilter = (
448
+ ~loadManager,
449
+ ~persistence,
450
+ ~entityConfig: Internal.entityConfig,
451
+ ~scope,
452
+ ~indexerState,
453
+ ~shouldGroup,
454
+ ~item,
455
+ ~ecosystem,
456
+ ~filter: dict<dict<unknown>>,
457
+ ) =>
458
+ // Rejecting rather than throwing keeps a bad filter failing only its own
459
+ // call, even when the caller batches several with Promise.all.
460
+ try {
461
+ loadByParsedFilter(
462
+ ~loadManager,
463
+ ~persistence,
464
+ ~entityConfig,
465
+ ~scope,
466
+ ~indexerState,
467
+ ~shouldGroup,
468
+ ~item,
469
+ ~ecosystem,
470
+ ~filter=filter->EntityFilter.parseOrThrow(
471
+ ~entityName=entityConfig.name,
472
+ ~table=entityConfig.table,
473
+ ),
474
+ )
475
+ } catch {
476
+ | exn => Promise.reject(exn->Utils.prettifyExn)
477
+ }
@@ -1,9 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Table from "./db/Table.res.mjs";
4
3
  import * as Utils from "./Utils.res.mjs";
5
4
  import * as ChainId from "./ChainId.res.mjs";
6
5
  import * as Logging from "./Logging.res.mjs";
6
+ import * as Internal from "./Internal.res.mjs";
7
7
  import * as Ecosystem from "./Ecosystem.res.mjs";
8
8
  import * as EffectState from "./EffectState.res.mjs";
9
9
  import * as LoadManager from "./LoadManager.res.mjs";
@@ -26,25 +26,6 @@ function scopeKeySuffix(scope) {
26
26
  }
27
27
  }
28
28
 
29
- function scopeFilter(filter, table, scope) {
30
- let match = Table.getChainIdField(table);
31
- if (scope === "crossChain" || match === undefined) {
32
- return filter;
33
- } else {
34
- return {
35
- operator: "and",
36
- filters: [
37
- filter,
38
- {
39
- operator: "=",
40
- fieldName: match.fieldName,
41
- fieldValue: scope
42
- }
43
- ]
44
- };
45
- }
46
- }
47
-
48
29
  function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, entityId) {
49
30
  let key = entityConfig.name + `.get` + scopeKeySuffix(scope);
50
31
  let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
@@ -53,11 +34,7 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
53
34
  let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
54
35
  let dbEntities;
55
36
  try {
56
- dbEntities = await storage.loadOrThrow(scopeFilter({
57
- operator: "in",
58
- fieldName: Table.idFieldName,
59
- fieldValue: idsToLoad
60
- }, entityConfig.table, scope), entityConfig.table);
37
+ dbEntities = await storage.loadOrThrow(EntityFilter.scoped(EntityFilter.byIds(idsToLoad), entityConfig.table, scope), entityConfig.table);
61
38
  } catch (raw_exn) {
62
39
  let exn = Primitive_exceptions.internalToException(raw_exn);
63
40
  if (exn.RE_EXN_ID === Persistence.StorageError) {
@@ -71,7 +48,8 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
71
48
  let entity = dbEntities[idx];
72
49
  entitiesMap[entity.id] = entity;
73
50
  }
74
- idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, IndexerState.committedCheckpointId(indexerState), entityId, entitiesMap[entityId]));
51
+ let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
52
+ idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityId]));
75
53
  return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, idsToLoad.length, dbEntities.length);
76
54
  };
77
55
  return LoadManager.call(loadManager, entityId, key, load, LoadManager.noopHasher, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
@@ -79,7 +57,7 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
79
57
 
80
58
  function callEffect(effect, arg, inMemTable, timerRef, onError) {
81
59
  EffectState.startCall(inMemTable.stats, timerRef);
82
- return effect.handler(arg).then(output => InMemoryStore.setEffectOutput(inMemTable, arg.checkpointId, arg.cacheKey, output, arg.context.cache)).catch(exn => onError(arg.cacheKey, exn)).finally(() => EffectState.endCall(inMemTable.stats, timerRef));
60
+ return effect.handler(arg).then(output => InMemoryStore.setEffectOutput(inMemTable, arg.chainId, arg.checkpointId, arg.cacheKey, output, arg.context.cache)).catch(exn => onError(arg.cacheKey, exn)).finally(() => EffectState.endCall(inMemTable.stats, timerRef));
83
61
  }
84
62
 
85
63
  function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQueue) {
@@ -155,11 +133,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
155
133
  let outputSchema = match$1.outputSchema;
156
134
  let dbEntities;
157
135
  try {
158
- dbEntities = await storage.loadOrThrow({
159
- operator: "in",
160
- fieldName: Table.idFieldName,
161
- fieldValue: idsToLoad
162
- }, table);
136
+ dbEntities = await storage.loadOrThrow(EntityFilter.byIds(idsToLoad), table);
163
137
  } catch (raw_exn) {
164
138
  let exn = Primitive_exceptions.internalToException(raw_exn);
165
139
  Logging.childWarn(Ecosystem.getItemLogger(item, ecosystem), {
@@ -173,7 +147,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
173
147
  try {
174
148
  let output = S$RescriptSchema.parseOrThrow(dbEntity.output, outputSchema);
175
149
  idsFromCache.add(dbEntity.id);
176
- return InMemoryStore.initEffectOutputFromDb(inMemTable, dbEntity.id, output);
150
+ return InMemoryStore.initEffectOutputFromDb(inMemTable, Internal.getItemChainId(item), dbEntity.id, output);
177
151
  } catch (raw_error) {
178
152
  let error = Primitive_exceptions.internalToException(raw_error);
179
153
  if (error.RE_EXN_ID === S$RescriptSchema.Raised) {
@@ -209,37 +183,49 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
209
183
  }
210
184
 
211
185
  function loadByFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, filter) {
212
- let key = EntityFilter.toOperationKey(filter, entityConfig.name) + scopeKeySuffix(scope);
213
- let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
214
- let load = async (filters, param) => {
215
- let storage = Persistence.getInitializedStorageOrThrow(persistence);
216
- let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
217
- let size = {
218
- contents: 0
219
- };
220
- filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter));
221
- await storage.ensureQueryIndexes(entityConfig.table, filters);
222
- let queries = EntityFilter.merge(filters);
223
- await Promise.all(queries.map(async filter => {
224
- try {
225
- let entities = await storage.loadOrThrow(scopeFilter(filter, entityConfig.table, scope), entityConfig.table);
226
- entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, IndexerState.committedCheckpointId(indexerState), entity.id, entity));
227
- size.contents = size.contents + entities.length | 0;
228
- return;
229
- } catch (raw_exn) {
230
- let exn = Primitive_exceptions.internalToException(raw_exn);
231
- if (exn.RE_EXN_ID === Persistence.StorageError) {
232
- return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
233
- operation: key,
234
- params: EntityFilter.getParams(filter)
235
- }), exn.message, exn.reason);
186
+ try {
187
+ let filter$1 = EntityFilter.parseOrThrow(filter, entityConfig.name, entityConfig.table);
188
+ let key = EntityFilter.toOperationKey(filter$1, entityConfig.name) + scopeKeySuffix(scope);
189
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
190
+ let load = async (filters, param) => {
191
+ let storage = Persistence.getInitializedStorageOrThrow(persistence);
192
+ let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
193
+ let size = {
194
+ contents: 0
195
+ };
196
+ filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter, entityConfig.table));
197
+ await storage.ensureQueryIndexes(entityConfig, scope, filters);
198
+ let queries = EntityFilter.merge(filters);
199
+ await Promise.all(queries.map(async filter => {
200
+ try {
201
+ let entities = await storage.loadOrThrow(EntityFilter.scoped(filter, entityConfig.table, scope), entityConfig.table);
202
+ let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
203
+ entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
204
+ InMemoryTable.Entity.recordLoadedValues(inMemTable, filter, entityConfig.table);
205
+ size.contents = size.contents + entities.length | 0;
206
+ return;
207
+ } catch (raw_exn) {
208
+ let exn = Primitive_exceptions.internalToException(raw_exn);
209
+ if (exn.RE_EXN_ID === Persistence.StorageError) {
210
+ return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
211
+ operation: key,
212
+ params: EntityFilter.getParams(filter)
213
+ }), exn.message, exn.reason);
214
+ }
215
+ throw exn;
236
216
  }
237
- throw exn;
238
- }
239
- }));
240
- return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
241
- };
242
- return LoadManager.call(loadManager, filter, key, load, EntityFilter.toString, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
217
+ }));
218
+ return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
219
+ };
220
+ let filterKey = EntityFilter.toString(filter$1, entityConfig.table);
221
+ if (!InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
222
+ InMemoryTable.Entity.tryIndexFromLoadedValues(inMemTable, filter$1, entityConfig.table);
223
+ }
224
+ return LoadManager.call(loadManager, filter$1, key, load, param => filterKey, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
225
+ } catch (raw_exn) {
226
+ let exn = Primitive_exceptions.internalToException(raw_exn);
227
+ return Promise.reject(Utils.prettifyExn(exn));
228
+ }
243
229
  }
244
230
 
245
231
  export {
@@ -247,4 +233,4 @@ export {
247
233
  loadByFilter,
248
234
  loadEffect,
249
235
  }
250
- /* Table Not a pure module */
236
+ /* Utils Not a pure module */
@@ -19,7 +19,7 @@ let loadByFilter: (
19
19
  ~shouldGroup: bool,
20
20
  ~item: Internal.item,
21
21
  ~ecosystem: Ecosystem.t,
22
- ~filter: EntityFilter.t,
22
+ ~filter: dict<dict<unknown>>,
23
23
  ) => promise<array<Internal.entity>>
24
24
 
25
25
  let loadEffect: (
package/src/Main.res CHANGED
@@ -488,7 +488,12 @@ let getGlobalIndexer = (): 'indexer => {
488
488
  Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
489
489
  }
490
490
 
491
- let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: bool) => {
491
+ let startServer = (
492
+ ~getMetrics: unit => option<Metrics.t>,
493
+ ~envioVersion: string,
494
+ ~persistence: Persistence.t,
495
+ ~isDevelopmentMode: bool,
496
+ ) => {
492
497
  open Express
493
498
 
494
499
  let app = make()
@@ -520,10 +525,20 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
520
525
  })
521
526
 
522
527
  app->get("/console/state", (_req, res) => {
523
- let state = if isDevelopmentMode {
524
- getState()
525
- } else {
528
+ let state = if !isDevelopmentMode {
526
529
  Disabled({})
530
+ } else {
531
+ switch getMetrics() {
532
+ | None => Initializing({})
533
+ | Some(metrics) =>
534
+ Active({
535
+ envioVersion,
536
+ chains: metrics.chains->Array.map(toChainData),
537
+ indexerStartTime: metrics.startTime,
538
+ isPreRegisteringDynamicContracts: false,
539
+ rollbackOnReorg: metrics.rollbackEnabled,
540
+ })
541
+ }
527
542
  }
528
543
 
529
544
  res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
@@ -543,10 +558,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
543
558
 
544
559
  app->get("/metrics", (_req, res) => {
545
560
  res->set("Content-Type", Metrics.contentType)
546
- let _ =
547
- res->endWithData(
548
- Metrics.collect(~metrics=getIndexerState()->Option.map(IndexerState.toMetrics)),
549
- )
561
+ let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
550
562
  })
551
563
 
552
564
  app->get("/metrics/runtime", (_req, res) => {
@@ -648,6 +660,7 @@ let start = async (
648
660
  ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
649
661
  ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
650
662
  ~lowercaseAddresses=config.lowercaseAddresses,
663
+ ~requireInitialized=config.isolated,
651
664
  )
652
665
 
653
666
  // Loads user handler files, which register handler/contractRegister/where
@@ -683,22 +696,10 @@ let start = async (
683
696
  }
684
697
  let envioVersion = Utils.EnvioPackage.value.version
685
698
 
699
+ let getMetrics = () => getIndexerState()->Option.map(IndexerState.toMetrics)
700
+
686
701
  if !isTest {
687
- startServer(~persistence, ~isDevelopmentMode, ~getState=() =>
688
- switch getIndexerState() {
689
- | None => Initializing({})
690
- | Some(state) => {
691
- let chains = (state->IndexerState.toMetrics).chains->Array.map(toChainData)
692
- Active({
693
- envioVersion,
694
- chains,
695
- indexerStartTime: state->IndexerState.indexerStartTime,
696
- isPreRegisteringDynamicContracts: false,
697
- rollbackOnReorg: config.shouldRollbackOnReorg,
698
- })
699
- }
700
- }
701
- )
702
+ startServer(~persistence, ~isDevelopmentMode, ~envioVersion, ~getMetrics)
702
703
  }
703
704
 
704
705
  let state = IndexerState.makeFromDbState(
@@ -712,7 +713,7 @@ let start = async (
712
713
  ~onError,
713
714
  )
714
715
  if shouldUseTui {
715
- let _rerender = Tui.start(~getState=() => state)
716
+ let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
716
717
  }
717
718
  setIndexerState(state)
718
719
  state->IndexerLoop.start
package/src/Main.res.mjs CHANGED
@@ -402,7 +402,7 @@ function getGlobalIndexer() {
402
402
  return new Proxy(Object.create(null), traps);
403
403
  }
404
404
 
405
- function startServer(getState, persistence, isDevelopmentMode) {
405
+ function startServer(getMetrics, envioVersion, persistence, isDevelopmentMode) {
406
406
  let app = Express();
407
407
  let consoleCorsMiddleware = (req, res, next) => {
408
408
  let origin = req.headers["origin"];
@@ -425,9 +425,24 @@ function startServer(getState, persistence, isDevelopmentMode) {
425
425
  res.sendStatus(200);
426
426
  });
427
427
  app.get("/console/state", (_req, res) => {
428
- let state = isDevelopmentMode ? getState() : ({
428
+ let state;
429
+ if (isDevelopmentMode) {
430
+ let metrics = getMetrics();
431
+ state = metrics !== undefined ? ({
432
+ status: "active",
433
+ envioVersion: envioVersion,
434
+ chains: metrics.chains.map(toChainData),
435
+ indexerStartTime: metrics.startTime,
436
+ isPreRegisteringDynamicContracts: false,
437
+ rollbackOnReorg: metrics.rollbackEnabled
438
+ }) : ({
439
+ status: "initializing"
440
+ });
441
+ } else {
442
+ state = {
429
443
  status: "disabled"
430
- });
444
+ };
445
+ }
431
446
  res.json(S$RescriptSchema.reverseConvertToJsonOrThrow(state, stateSchema));
432
447
  });
433
448
  app.post("/console/syncCache", (_req, res) => {
@@ -442,7 +457,7 @@ function startServer(getState, persistence, isDevelopmentMode) {
442
457
  Metrics.startRuntimeCollectors();
443
458
  app.get("/metrics", (_req, res) => {
444
459
  res.set("Content-Type", Metrics.contentType);
445
- res.end(Metrics.collect(Stdlib_Option.map(EnvioGlobal.value.indexerState, IndexerState.toMetrics)));
460
+ res.end(Metrics.collect(getMetrics()));
446
461
  });
447
462
  app.get("/metrics/runtime", (_req, res) => {
448
463
  res.set("Content-Type", Metrics.contentType);
@@ -467,7 +482,7 @@ function getEnvioInfo() {
467
482
  async function migrate(reset) {
468
483
  let config = Config.load();
469
484
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
470
- await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, "Once");
485
+ await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, undefined, "Once");
471
486
  return await persistence.storage.close();
472
487
  }
473
488
 
@@ -494,7 +509,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
494
509
  let isDevelopmentMode = !isTest && config.isDev;
495
510
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
496
511
  EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
497
- await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, undefined);
512
+ await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, config.isolated, undefined);
498
513
  let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
499
514
  let config$1;
500
515
  if (isTest) {
@@ -519,29 +534,13 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
519
534
  });
520
535
  };
521
536
  let envioVersion = Utils.EnvioPackage.value.version;
537
+ let getMetrics = () => Stdlib_Option.map(EnvioGlobal.value.indexerState, IndexerState.toMetrics);
522
538
  if (!isTest) {
523
- startServer(() => {
524
- let state = EnvioGlobal.value.indexerState;
525
- if (state === undefined) {
526
- return {
527
- status: "initializing"
528
- };
529
- }
530
- let state$1 = Primitive_option.valFromOption(state);
531
- let chains = IndexerState.toMetrics(state$1).chains.map(toChainData);
532
- return {
533
- status: "active",
534
- envioVersion: envioVersion,
535
- chains: chains,
536
- indexerStartTime: IndexerState.indexerStartTime(state$1),
537
- isPreRegisteringDynamicContracts: false,
538
- rollbackOnReorg: config$2.shouldRollbackOnReorg
539
- };
540
- }, persistence$1, isDevelopmentMode);
539
+ startServer(getMetrics, envioVersion, persistence$1, isDevelopmentMode);
541
540
  }
542
541
  let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError, undefined);
543
542
  if (shouldUseTui) {
544
- Tui.start(() => state);
543
+ Tui.start(config$2, () => IndexerState.toMetrics(state));
545
544
  }
546
545
  EnvioGlobal.value.indexerState = Primitive_option.some(state);
547
546
  IndexerLoop.start(state);
package/src/Metrics.res CHANGED
@@ -20,6 +20,7 @@ type chainMetrics = {
20
20
  // Raw committed progress (may be -1), unlike the optional latestProcessedBlock.
21
21
  progressBlockNumber: int,
22
22
  progressLatencyMs: option<int>,
23
+ progressBlockTime: option<int>,
23
24
  concurrency: int,
24
25
  partitionsCount: int,
25
26
  bufferSize: int,
@@ -36,8 +37,18 @@ type chainMetrics = {
36
37
  reorgCount: int,
37
38
  reorgDetectedBlock: option<int>,
38
39
  rollbackTargetBlock: option<int>,
40
+ rateLimitTimeMs: float,
41
+ rateLimitResetInMs: option<float>,
39
42
  }
40
43
 
44
+ // Mirrors `ChainState.hasProcessedToEndblock`, which is what clamps
45
+ // `knownHeight` to the end block — the two must agree.
46
+ let hasProcessedToEndblock = (m: chainMetrics) =>
47
+ switch m.endBlock {
48
+ | Some(endBlock) => m.progressBlockNumber >= endBlock
49
+ | None => false
50
+ }
51
+
41
52
  type handlerMetrics = {
42
53
  contract: string,
43
54
  event: string,
@@ -90,6 +101,10 @@ type sourceRequestMetrics = {
90
101
  method: string,
91
102
  count: int,
92
103
  seconds: float,
104
+ // None for methods whose responses aren't measured in blocks; they render no
105
+ // block series at all.
106
+ responseBlocks: option<int>,
107
+ emptyResponseCount: int,
93
108
  }
94
109
 
95
110
  type sourceHeightMetrics = {
@@ -249,7 +264,16 @@ let renderMetrics = (b: builder, metrics: t) => {
249
264
  | Some(existing) =>
250
265
  byLabels->Dict.set(
251
266
  labels,
252
- {...existing, count: existing.count + s.count, seconds: existing.seconds +. s.seconds},
267
+ {
268
+ ...existing,
269
+ count: existing.count + s.count,
270
+ seconds: existing.seconds +. s.seconds,
271
+ responseBlocks: switch (existing.responseBlocks, s.responseBlocks) {
272
+ | (Some(a), Some(b)) => Some(a + b)
273
+ | (some, None) | (None, some) => some
274
+ },
275
+ emptyResponseCount: existing.emptyResponseCount + s.emptyResponseCount,
276
+ },
253
277
  )
254
278
  | None => byLabels->Dict.set(labels, s)
255
279
  }
@@ -509,6 +533,23 @@ let renderMetrics = (b: builder, metrics: t) => {
509
533
  ~entries=sourceRequests,
510
534
  ~value=s => s.seconds !== 0. ? Some(s.seconds) : None,
511
535
  )
536
+ b->seriesOpt(
537
+ ~name="envio_source_response_blocks_total",
538
+ ~help="The number of blocks a data source returned, summed over its responses. Counted as the source sent them, before the indexer drops the blocks no event needs. Only sources whose responses are measured in blocks report it.",
539
+ ~kind="counter",
540
+ ~entries=sourceRequests,
541
+ ~value=s => s.responseBlocks->Option.map(Int.toFloat),
542
+ )
543
+ // Rendered flat at zero for any method that reports blocks: a chain scanning
544
+ // ranges it finds nothing in is the reading this exists for, and a series
545
+ // that only appears once the first empty response lands can't be alerted on.
546
+ b->seriesOpt(
547
+ ~name="envio_source_response_empty_total",
548
+ ~help="The number of responses that came back with no blocks at all — a range the source scanned and matched nothing in. Compare against envio_source_request_total for the share of requests that returned nothing.",
549
+ ~kind="counter",
550
+ ~entries=sourceRequests,
551
+ ~value=s => s.responseBlocks->Option.map(_ => s.emptyResponseCount->Int.toFloat),
552
+ )
512
553
  if heightStreamConnects->Array.length > 0 {
513
554
  b->series(
514
555
  ~name="envio_source_height_stream_connects_total",
@@ -619,6 +660,13 @@ let renderMetrics = (b: builder, metrics: t) => {
619
660
  ~entries=chains,
620
661
  ~value=m => m.numEventsProcessed,
621
662
  )
663
+ b->seriesOpt(
664
+ ~name="envio_progress_block_time_seconds",
665
+ ~help="Unix timestamp of the block the chain has processed up to. Subtract it from the scrape time for how far behind chain time the indexer is, which stays honest when the data source itself is behind the chain. Best effort in realtime mode, absent during backfill.",
666
+ ~kind="gauge",
667
+ ~entries=chains,
668
+ ~value=m => m.progressBlockTime->Option.map(Int.toFloat),
669
+ )
622
670
  b->seriesOpt(
623
671
  ~name="envio_progress_latency",
624
672
  ~help="The latency in milliseconds between the latest processed event creation and the time it was written to storage.",