envio 3.3.0-alpha.8 → 3.3.0-rc.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.
Files changed (107) hide show
  1. package/index.d.ts +19 -0
  2. package/licenses/CLA.md +35 -0
  3. package/licenses/EULA.md +67 -0
  4. package/licenses/LICENSE.md +45 -0
  5. package/licenses/README.md +35 -0
  6. package/package.json +9 -8
  7. package/src/Batch.res.mjs +1 -1
  8. package/src/BatchProcessing.res +0 -7
  9. package/src/BatchProcessing.res.mjs +1 -8
  10. package/src/ChainFetching.res +30 -18
  11. package/src/ChainFetching.res.mjs +23 -13
  12. package/src/ChainState.res +205 -59
  13. package/src/ChainState.res.mjs +130 -27
  14. package/src/ChainState.resi +16 -1
  15. package/src/Config.res +9 -5
  16. package/src/Config.res.mjs +2 -2
  17. package/src/Core.res +20 -0
  18. package/src/Core.res.mjs +12 -0
  19. package/src/CrossChainState.res +147 -39
  20. package/src/CrossChainState.res.mjs +42 -10
  21. package/src/EffectState.res +100 -0
  22. package/src/EffectState.res.mjs +74 -0
  23. package/src/EffectState.resi +32 -0
  24. package/src/Envio.res +25 -7
  25. package/src/Envio.res.mjs +15 -19
  26. package/src/EnvioGlobal.res +53 -0
  27. package/src/EnvioGlobal.res.mjs +31 -0
  28. package/src/EventConfigBuilder.res +30 -56
  29. package/src/EventConfigBuilder.res.mjs +24 -30
  30. package/src/EventProcessing.res +8 -5
  31. package/src/EventProcessing.res.mjs +2 -1
  32. package/src/FetchState.res +842 -474
  33. package/src/FetchState.res.mjs +545 -356
  34. package/src/HandlerLoader.res +1 -1
  35. package/src/HandlerLoader.res.mjs +1 -1
  36. package/src/HandlerRegister.res +478 -305
  37. package/src/HandlerRegister.res.mjs +277 -209
  38. package/src/HandlerRegister.resi +11 -6
  39. package/src/InMemoryStore.res +14 -26
  40. package/src/InMemoryStore.res.mjs +6 -19
  41. package/src/IndexerState.res +15 -39
  42. package/src/IndexerState.res.mjs +18 -29
  43. package/src/IndexerState.resi +2 -10
  44. package/src/Internal.res +121 -18
  45. package/src/Internal.res.mjs +96 -2
  46. package/src/LoadLayer.res +44 -24
  47. package/src/LoadLayer.res.mjs +43 -20
  48. package/src/LoadLayer.resi +1 -0
  49. package/src/LogSelection.res +92 -217
  50. package/src/LogSelection.res.mjs +95 -184
  51. package/src/Main.res +29 -143
  52. package/src/Main.res.mjs +25 -88
  53. package/src/Metrics.res +1 -1
  54. package/src/Metrics.res.mjs +1 -1
  55. package/src/Persistence.res +12 -3
  56. package/src/PgStorage.res +155 -82
  57. package/src/PgStorage.res.mjs +130 -77
  58. package/src/Prometheus.res +20 -10
  59. package/src/Prometheus.res.mjs +22 -10
  60. package/src/PruneStaleHistory.res +146 -35
  61. package/src/PruneStaleHistory.res.mjs +114 -20
  62. package/src/RawEvent.res +2 -2
  63. package/src/Rollback.res +32 -13
  64. package/src/Rollback.res.mjs +24 -11
  65. package/src/RollbackCommit.res +4 -1
  66. package/src/RollbackCommit.res.mjs +3 -2
  67. package/src/SimulateDeadInputTracker.res +1 -1
  68. package/src/SimulateItems.res +39 -7
  69. package/src/SimulateItems.res.mjs +28 -9
  70. package/src/TestIndexer.res +2 -2
  71. package/src/TestIndexer.res.mjs +2 -2
  72. package/src/TopicFilter.res +1 -25
  73. package/src/TopicFilter.res.mjs +0 -74
  74. package/src/UserContext.res +62 -23
  75. package/src/UserContext.res.mjs +26 -6
  76. package/src/Writing.res +60 -21
  77. package/src/Writing.res.mjs +27 -9
  78. package/src/bindings/NodeJs.res +5 -0
  79. package/src/bindings/Viem.res +0 -5
  80. package/src/sources/EventRouter.res +0 -21
  81. package/src/sources/EventRouter.res.mjs +0 -12
  82. package/src/sources/Evm.res +4 -1
  83. package/src/sources/Evm.res.mjs +1 -1
  84. package/src/sources/EvmChain.res +2 -27
  85. package/src/sources/EvmChain.res.mjs +2 -23
  86. package/src/sources/EvmRpcClient.res +58 -23
  87. package/src/sources/EvmRpcClient.res.mjs +11 -5
  88. package/src/sources/Fuel.res +3 -1
  89. package/src/sources/Fuel.res.mjs +1 -1
  90. package/src/sources/HyperSync.res +9 -38
  91. package/src/sources/HyperSync.res.mjs +16 -28
  92. package/src/sources/HyperSync.resi +2 -2
  93. package/src/sources/HyperSyncClient.res +88 -11
  94. package/src/sources/HyperSyncClient.res.mjs +39 -6
  95. package/src/sources/HyperSyncSource.res +18 -210
  96. package/src/sources/HyperSyncSource.res.mjs +9 -137
  97. package/src/sources/Rpc.res +0 -32
  98. package/src/sources/Rpc.res.mjs +1 -46
  99. package/src/sources/RpcSource.res +129 -533
  100. package/src/sources/RpcSource.res.mjs +161 -379
  101. package/src/sources/SimulateSource.res +37 -19
  102. package/src/sources/SimulateSource.res.mjs +27 -10
  103. package/src/sources/SourceManager.res +4 -14
  104. package/src/sources/SourceManager.res.mjs +3 -9
  105. package/src/sources/SourceManager.resi +0 -2
  106. package/src/sources/SvmHyperSyncSource.res +13 -3
  107. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
@@ -1,79 +1,28 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Utils from "./Utils.res.mjs";
4
+ import * as TopicFilter from "./TopicFilter.res.mjs";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
7
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
8
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
8
9
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
9
10
 
10
- let MissingRequiredTopic0 = /* @__PURE__ */Primitive_exceptions.create("LogSelection.MissingRequiredTopic0");
11
-
12
- function makeTopicSelection(topic0, topic1Opt, topic2Opt, topic3Opt) {
13
- let topic1 = topic1Opt !== undefined ? topic1Opt : [];
14
- let topic2 = topic2Opt !== undefined ? topic2Opt : [];
15
- let topic3 = topic3Opt !== undefined ? topic3Opt : [];
16
- if (Utils.$$Array.isEmpty(topic0)) {
17
- return {
18
- TAG: "Error",
19
- _0: {
20
- RE_EXN_ID: MissingRequiredTopic0
21
- }
22
- };
11
+ function materializeTopicFilter(filter, addresses) {
12
+ if (filter.TAG === "Values") {
13
+ return filter._0;
23
14
  } else {
24
- return {
25
- TAG: "Ok",
26
- _0: {
27
- topic0: topic0,
28
- topic1: topic1,
29
- topic2: topic2,
30
- topic3: topic3
31
- }
32
- };
15
+ return addresses.map(TopicFilter.fromAddress);
33
16
  }
34
17
  }
35
18
 
36
- function hasFilters(param) {
37
- return Stdlib_Option.isSome([
38
- param.topic1,
39
- param.topic2,
40
- param.topic3
41
- ].find(topic => !Utils.$$Array.isEmpty(topic)));
42
- }
43
-
44
- function compressTopicSelections(topicSelections) {
45
- let topic0sOfSelectionsWithoutFilters = [];
46
- let selectionsWithFilters = [];
47
- topicSelections.forEach(selection => {
48
- if (hasFilters(selection)) {
49
- selectionsWithFilters.push(selection);
50
- } else {
51
- selection.topic0.forEach(topic0 => {
52
- topic0sOfSelectionsWithoutFilters.push(topic0);
53
- });
54
- }
55
- });
56
- if (topic0sOfSelectionsWithoutFilters.length === 0) {
57
- return selectionsWithFilters;
58
- }
59
- let selectionWithoutFilters_topic1 = [];
60
- let selectionWithoutFilters_topic2 = [];
61
- let selectionWithoutFilters_topic3 = [];
62
- let selectionWithoutFilters = {
63
- topic0: topic0sOfSelectionsWithoutFilters,
64
- topic1: selectionWithoutFilters_topic1,
65
- topic2: selectionWithoutFilters_topic2,
66
- topic3: selectionWithoutFilters_topic3
67
- };
68
- return [selectionWithoutFilters].concat(selectionsWithFilters);
69
- }
70
-
71
- function make(addresses, topicSelections) {
72
- let topicSelections$1 = compressTopicSelections(topicSelections);
73
- return {
74
- addresses: addresses,
75
- topicSelections: topicSelections$1
76
- };
19
+ function materializeTopicSelections(topicSelections, addresses) {
20
+ return topicSelections.map(param => ({
21
+ topic0: param.topic0,
22
+ topic1: materializeTopicFilter(param.topic1, addresses),
23
+ topic2: materializeTopicFilter(param.topic2, addresses),
24
+ topic3: materializeTopicFilter(param.topic3, addresses)
25
+ }));
77
26
  }
78
27
 
79
28
  let eventBlockRangeSchema = S$RescriptSchema.strict(S$RescriptSchema.object(s => ({
@@ -100,16 +49,7 @@ function extractStartBlock(where, onEventBlockFilterSchema, contractName) {
100
49
  }
101
50
  }
102
51
 
103
- function makeChainArg(contractName, chainId, addresses) {
104
- let chainObj = {};
105
- chainObj["id"] = chainId;
106
- chainObj[contractName] = {
107
- addresses: addresses
108
- };
109
- return chainObj;
110
- }
111
-
112
- function makeDetectionChainArg(contractName, chainId, getAddresses) {
52
+ function makeChainArg(contractName, chainId, getAddresses) {
113
53
  let contractObj = Object.create(null);
114
54
  Object.defineProperty(contractObj, "addresses", {
115
55
  enumerable: true,
@@ -133,85 +73,78 @@ let makeAddressesProbe = (function (contractName) {
133
73
  return new Proxy([], {get: trap});
134
74
  });
135
75
 
136
- function extractAddressFilterGroupsOrThrow(result, probe, contractName) {
137
- let groups = [];
138
- let scanGroup = paramsObj => {
139
- let names = [];
140
- Utils.Dict.forEachWithKey(paramsObj, (value, key) => {
141
- if (value === probe) {
142
- names.push(key);
143
- paramsObj[key] = [];
144
- return;
145
- }
146
- });
147
- if (!Utils.$$Array.isEmpty(names)) {
148
- groups.push(names);
149
- return;
150
- }
151
- };
152
- if (typeof result === "object" && result !== null && !Array.isArray(result)) {
153
- let match = result["params"];
154
- if (match !== undefined) {
155
- if (Array.isArray(match)) {
156
- match.forEach(item => {
157
- if (typeof item === "object" && item !== null && !Array.isArray(item)) {
158
- return scanGroup(item);
159
- }
160
- });
161
- } else {
162
- switch (typeof match) {
163
- case "object" :
164
- scanGroup(match);
165
- break;
166
- }
167
- }
168
- }
169
- }
170
- if (Utils.$$Array.isEmpty(groups)) {
171
- Stdlib_JsError.throwWithMessage(`Invalid where configuration for ` + contractName + `. The callback reads \`chain.` + contractName + `.addresses\` but doesn't use it as an indexed-param filter value. Use it directly, e.g. { params: { to: chain.` + contractName + `.addresses } }.`);
172
- }
173
- return groups;
174
- }
175
-
176
76
  let emptyTopics = [];
177
77
 
178
78
  function noopGetter(param) {
179
79
  return emptyTopics;
180
80
  }
181
81
 
182
- function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, probeChainId, onEventBlockFilterSchema, topic1Opt, topic2Opt, topic3Opt) {
82
+ function parseWhereOrThrow(where, sighash, params, contractName, chainId, onEventBlockFilterSchema, topic1Opt, topic2Opt, topic3Opt) {
183
83
  let topic1 = topic1Opt !== undefined ? topic1Opt : noopGetter;
184
84
  let topic2 = topic2Opt !== undefined ? topic2Opt : noopGetter;
185
85
  let topic3 = topic3Opt !== undefined ? topic3Opt : noopGetter;
186
- let filterByAddresses = {
86
+ let addressFilterParamGroups = [];
87
+ let readAddresses = {
187
88
  contents: false
188
89
  };
189
- let addressFilterParamGroups = [];
190
- let startBlock;
90
+ let addressesSentinel = makeAddressesProbe(contractName);
191
91
  let topic0 = [sighash];
92
+ let default_topic1 = {
93
+ TAG: "Values",
94
+ _0: emptyTopics
95
+ };
96
+ let default_topic2 = {
97
+ TAG: "Values",
98
+ _0: emptyTopics
99
+ };
100
+ let default_topic3 = {
101
+ TAG: "Values",
102
+ _0: emptyTopics
103
+ };
192
104
  let $$default = {
193
105
  topic0: topic0,
194
- topic1: emptyTopics,
195
- topic2: emptyTopics,
196
- topic3: emptyTopics
106
+ topic1: default_topic1,
107
+ topic2: default_topic2,
108
+ topic3: default_topic3
197
109
  };
198
- let paramsRecordToTopicSelection = paramsFilter => {
199
- if (Utils.Dict.isEmpty(paramsFilter)) {
200
- return $$default;
110
+ let topicFilterAt = (paramsFilter, index, getter) => {
111
+ let value = Stdlib_Option.flatMap(params[index], name => paramsFilter[name]);
112
+ if (value !== undefined && value === addressesSentinel) {
113
+ return {
114
+ TAG: "ContractAddresses",
115
+ contractName: contractName
116
+ };
201
117
  } else {
202
- Utils.Dict.forEachWithKey(paramsFilter, (param, key) => {
203
- if (!params.includes(key)) {
204
- return Stdlib_JsError.throwWithMessage(`Invalid where configuration. The event doesn't have an indexed parameter "` + key + `" and can't use it for filtering`);
205
- }
206
- });
207
118
  return {
208
- topic0: topic0,
209
- topic1: topic1(paramsFilter),
210
- topic2: topic2(paramsFilter),
211
- topic3: topic3(paramsFilter)
119
+ TAG: "Values",
120
+ _0: getter(paramsFilter)
212
121
  };
213
122
  }
214
123
  };
124
+ let paramsRecordToTopicSelection = paramsFilter => {
125
+ if (Utils.Dict.isEmpty(paramsFilter)) {
126
+ return $$default;
127
+ }
128
+ let sentinelParamNames = [];
129
+ Utils.Dict.forEachWithKey(paramsFilter, (value, key) => {
130
+ if (!params.includes(key)) {
131
+ Stdlib_JsError.throwWithMessage(`Invalid where configuration. The event doesn't have an indexed parameter "` + key + `" and can't use it for filtering`);
132
+ }
133
+ if (value === addressesSentinel) {
134
+ sentinelParamNames.push(key);
135
+ return;
136
+ }
137
+ });
138
+ if (!Utils.$$Array.isEmpty(sentinelParamNames)) {
139
+ addressFilterParamGroups.push(sentinelParamNames);
140
+ }
141
+ return {
142
+ topic0: topic0,
143
+ topic1: topicFilterAt(paramsFilter, 0, topic1),
144
+ topic2: topicFilterAt(paramsFilter, 1, topic2),
145
+ topic3: topicFilterAt(paramsFilter, 2, topic3)
146
+ };
147
+ };
215
148
  let acceptedWhereKeys = [
216
149
  "params",
217
150
  "block"
@@ -258,73 +191,51 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
258
191
  return Stdlib_JsError.throwWithMessage("Invalid where configuration. Expected `params` to be an object or an array of objects");
259
192
  }
260
193
  };
261
- let getEventFiltersOrThrow;
262
- if (eventFilters !== undefined) {
263
- if (typeof eventFilters === "function") {
264
- let addressesProbe = makeAddressesProbe(contractName);
265
- let chain = makeDetectionChainArg(contractName, probeChainId, () => {
266
- filterByAddresses.contents = true;
267
- return addressesProbe;
194
+ let match;
195
+ if (where !== undefined) {
196
+ let whereValue;
197
+ if (typeof where === "function") {
198
+ let chain = makeChainArg(contractName, chainId, () => {
199
+ readAddresses.contents = true;
200
+ return addressesSentinel;
268
201
  });
269
- let probedResult = eventFilters({
202
+ whereValue = where({
270
203
  chain: chain
271
204
  });
272
- if (filterByAddresses.contents) {
273
- addressFilterParamGroups = extractAddressFilterGroupsOrThrow(probedResult, addressesProbe, contractName);
274
- }
275
- startBlock = extractStartBlock(probedResult, onEventBlockFilterSchema, contractName);
276
- getEventFiltersOrThrow = filterByAddresses.contents ? chain => ({
277
- TAG: "Dynamic",
278
- _0: addresses => {
279
- let chainArg = makeChainArg(contractName, chain, addresses);
280
- return parse(eventFilters({
281
- chain: chainArg
282
- }));
283
- }
284
- }) : chain => {
285
- let chainArg = makeDetectionChainArg(contractName, chain, () => Stdlib_JsError.throwWithMessage(`Invalid where configuration. Event callback for contract "` + contractName + `" read \`chain.` + contractName + `.addresses\` at runtime but the probe didn't detect the access on chainId ` + chain.toString() + `. Move the \`chain.` + contractName + `.addresses\` read above any \`chain.id\` branching so the probe picks up the dependency and switches to the dynamic fetch path.`));
286
- return {
287
- TAG: "Static",
288
- _0: parse(eventFilters({
289
- chain: chainArg
290
- }))
291
- };
292
- };
293
205
  } else {
294
- startBlock = extractStartBlock(eventFilters, onEventBlockFilterSchema, contractName);
295
- let $$static = {
296
- TAG: "Static",
297
- _0: parse(eventFilters)
298
- };
299
- getEventFiltersOrThrow = param => $$static;
206
+ whereValue = where;
300
207
  }
208
+ let topicSelections = parse(whereValue);
209
+ if (readAddresses.contents && Utils.$$Array.isEmpty(addressFilterParamGroups)) {
210
+ Stdlib_JsError.throwWithMessage(`Invalid where configuration for ` + contractName + `. The callback reads \`chain.` + contractName + `.addresses\` but doesn't use it as an indexed-param filter value. Use it directly, e.g. { params: { to: chain.` + contractName + `.addresses } }.`);
211
+ }
212
+ match = [
213
+ topicSelections,
214
+ extractStartBlock(whereValue, onEventBlockFilterSchema, contractName)
215
+ ];
301
216
  } else {
302
- let $$static$1 = {
303
- TAG: "Static",
304
- _0: [$$default]
305
- };
306
- getEventFiltersOrThrow = param => $$static$1;
217
+ match = [
218
+ [$$default],
219
+ undefined
220
+ ];
307
221
  }
308
222
  return {
309
- getEventFiltersOrThrow: getEventFiltersOrThrow,
310
- filterByAddresses: filterByAddresses.contents,
311
- addressFilterParamGroups: addressFilterParamGroups,
312
- startBlock: startBlock
223
+ resolvedWhere: {
224
+ topicSelections: match[0],
225
+ startBlock: match[1]
226
+ },
227
+ filterByAddresses: !Utils.$$Array.isEmpty(addressFilterParamGroups),
228
+ addressFilterParamGroups: addressFilterParamGroups
313
229
  };
314
230
  }
315
231
 
316
232
  export {
317
- MissingRequiredTopic0,
318
- makeTopicSelection,
319
- hasFilters,
320
- compressTopicSelections,
321
- make,
233
+ materializeTopicFilter,
234
+ materializeTopicSelections,
322
235
  eventBlockRangeSchema,
323
236
  extractStartBlock,
324
237
  makeChainArg,
325
- makeDetectionChainArg,
326
238
  makeAddressesProbe,
327
- extractAddressFilterGroupsOrThrow,
328
- parseEventFiltersOrThrow,
239
+ parseWhereOrThrow,
329
240
  }
330
241
  /* eventBlockRangeSchema Not a pure module */
package/src/Main.res CHANGED
@@ -39,40 +39,24 @@ let stateSchema = S.union([
39
39
  })),
40
40
  ])
41
41
 
42
- // Shape of the user-returned `{_gte?, _lte?, _every?}` filter chunk after
43
- // the ecosystem-specific wrapper is stripped. Shared across all ecosystems
44
- // the outer `block.number` / `block.height` / `slot` unwrap lives on each
45
- // ecosystem's `onBlockFilterSchema`, and the inner range fields are the
46
- // same everywhere.
47
- type blockRange = {
48
- _gte: option<int>,
49
- _lte: option<int>,
50
- _every: int,
51
- }
52
-
53
- // `S.strict` rejects unknown fields so typos like `_gt` / `_evry` surface
54
- // with a readable schema error pointing at the offending key, instead of
55
- // silently registering a broken filter. `_every` defaults to 1 inside the
56
- // schema so the caller always sees a plain `int`, and `intMin(1)` rejects
57
- // zero/negative strides — `(blockNumber - startBlock) % 0` would crash and
58
- // any negative stride would never match.
59
- let blockRangeSchema: S.t<blockRange> = S.object(s => {
60
- _gte: s.field("_gte", S.option(S.int)),
61
- _lte: s.field("_lte", S.option(S.int)),
62
- _every: s.field("_every", S.option(S.int->S.intMin(1))->S.Option.getOr(1)),
63
- })->S.strict
64
-
65
- let defaultBlockRange: blockRange = {_gte: None, _lte: None, _every: 1}
66
-
67
- let indexerStateRef: ref<option<IndexerState.t>> = ref(None)
42
+ // Runtime state lives in the process-wide `EnvioGlobal` record (shared
43
+ // across duplicate envio module instances); the slots are opaque there, so
44
+ // cast them to the real types here.
45
+ let getIndexerState = () =>
46
+ EnvioGlobal.value.indexerState->(Utils.magic: option<unknown> => option<IndexerState.t>)
47
+ let setIndexerState = (state: IndexerState.t) =>
48
+ EnvioGlobal.value.indexerState = Some(state->(Utils.magic: IndexerState.t => unknown))
68
49
 
69
50
  // Persistence is set by Main.start before handler modules load, so that
70
51
  // the exported indexer value can lazily expose DB state (startBlock,
71
52
  // endBlock, isRealtime, dynamic contract addresses) once it's ready.
72
- let globalPersistenceRef: ref<option<Persistence.t>> = ref(None)
53
+ let getGlobalPersistence = () =>
54
+ EnvioGlobal.value.persistence->(Utils.magic: option<unknown> => option<Persistence.t>)
55
+ let setGlobalPersistence = (persistence: Persistence.t) =>
56
+ EnvioGlobal.value.persistence = Some(persistence->(Utils.magic: Persistence.t => unknown))
73
57
 
74
58
  let getInitialChainState = (~chainId: int): option<Persistence.initialChainState> => {
75
- switch globalPersistenceRef.contents {
59
+ switch getGlobalPersistence() {
76
60
  | Some(persistence) =>
77
61
  switch persistence.storageStatus {
78
62
  | Ready(initialState) => initialState.chains->Array.find(c => c.id === chainId)
@@ -130,7 +114,7 @@ let buildChainsObject = (~config: Config.t) => {
130
114
  {
131
115
  enumerable: true,
132
116
  get: () => {
133
- switch indexerStateRef.contents {
117
+ switch getIndexerState() {
134
118
  | Some(state) => state->IndexerState.isRealtime
135
119
  // Before the global state is available (eg during handler
136
120
  // module load after resume), derive from persistence: every chain
@@ -164,7 +148,7 @@ let buildChainsObject = (~config: Config.t) => {
164
148
  {
165
149
  enumerable: true,
166
150
  get: () => {
167
- switch indexerStateRef.contents {
151
+ switch getIndexerState() {
168
152
  | Some(state) => {
169
153
  let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
170
154
  let chainState = state->IndexerState.getChainState(~chain)
@@ -345,121 +329,24 @@ let getGlobalIndexer = (): 'indexer => {
345
329
  let _ = RollbackCommit.register(callback->(Utils.magic: 'a => RollbackCommit.callback))
346
330
  }
347
331
 
348
- // Two-stage parse: first the ecosystem-specific outer schema unwraps the
349
- // wrapper (`block.number` / `block.height` / `slot`) and surfaces the
350
- // inner chunk as raw `unknown`; then the shared `blockRangeSchema`
351
- // validates the `{_gte?, _lte?, _every?}` fields. Keeping the inner
352
- // validation in one place means typos and shape mismatches surface with
353
- // the same user-friendly error regardless of ecosystem.
354
- let extractRange = (filter: unknown, ~name, ~ecosystem: Ecosystem.t): blockRange =>
355
- try {
356
- switch filter->S.parseOrThrow(ecosystem.onBlockFilterSchema) {
357
- | None => defaultBlockRange
358
- | Some(inner) => inner->S.parseOrThrow(blockRangeSchema)
359
- }
360
- } catch {
361
- | S.Raised(exn) =>
362
- JsError.throwWithMessage(
363
- `\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` returned an invalid filter: ${exn
364
- ->Utils.prettifyExn
365
- ->(Utils.magic: exn => string)}`,
366
- )
367
- }
368
-
369
- // `where` is evaluated once per configured chain at registration time.
370
- // Decoded ranges/stride feed directly into `HandlerRegister.registerOnBlock`
371
- // so the fetcher's `(blockNumber - handlerStartBlock) % interval === 0`
372
- // math at `FetchState.res:619` stays untouched.
373
332
  let onBlockFn = (rawOptions: 'a, handler: 'b) => {
374
333
  HandlerRegister.throwIfFinishedRegistration(~methodName="onBlock")
375
- let config = Config.load()
376
- let ecosystem = config.ecosystem
377
334
  let raw =
378
335
  rawOptions->(
379
336
  Utils.magic: 'a => {
380
337
  "name": string,
381
- "where": option<Envio.onBlockWhereArgs<unknown> => unknown>,
338
+ "where": unknown,
382
339
  }
383
340
  )
384
- let typedHandler = handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>)
385
- let (chains, _) = buildChainsObject(~config)
386
- let chainsDict = chains->(Utils.magic: {..} => dict<unknown>)
387
- let name = raw["name"]
388
- let logger = Logging.createChild(~params={"onBlock": name})
389
-
390
- // `where` must be a function (unlike onEvent, which also accepts a static
391
- // value). A static value would have to be evaluated against every chain
392
- // independently, which has no useful semantic for block handlers.
393
- // Normalize undefined/null to None up front so the per-chain loop below
394
- // can't accidentally call `null` as a predicate (ReScript treats a JS
395
- // `null` value as `Some(null)` when the field is typed as option).
396
- let where = switch raw["where"]->(Utils.magic: option<'a> => unknown) {
397
- | w if w === %raw(`undefined`) || w === %raw(`null`) => None
398
- | w if typeof(w) === #function => Some(raw["where"]->Option.getUnsafe)
399
- | w =>
400
- JsError.throwWithMessage(
401
- `\`indexer.${ecosystem.onBlockMethodName}("${name}")\` expected \`where\` to be a function or omitted, but got ${(typeof(
402
- w,
403
- ) :> string)}.`,
404
- )
405
- }
406
-
407
- let matchedAny = ref(false)
408
-
409
- config.chainMap
410
- ->ChainMap.values
411
- ->Array.forEach(chainConfig => {
412
- let chainId = chainConfig.id
413
- let chainObj = chainsDict->Dict.getUnsafe(chainId->Int.toString)
414
-
415
- // Predicate returns `true` → match with no filter; `false` → skip;
416
- // any plain object → structured filter. `undefined`/`null` returns
417
- // are rejected — the TS type excludes `void`, so a missing return is
418
- // a user bug we surface early rather than silently match-all.
419
- let result = switch where {
420
- | None => %raw(`true`)
421
- | Some(predicate) => predicate({chain: chainObj})
422
- }
423
-
424
- let (shouldRegister, range) = if result === %raw(`true`) {
425
- (true, defaultBlockRange)
426
- } else if result === %raw(`false`) {
427
- (false, defaultBlockRange)
428
- } else if typeof(result) === #object && !(result->Array.isArray) && result !== %raw(`null`) {
429
- (true, extractRange(result, ~name, ~ecosystem))
430
- } else {
431
- // Reject numbers, strings, functions, arrays, undefined, null —
432
- // anything that isn't bool or a plain object would silently
433
- // misregister.
434
- JsError.throwWithMessage(
435
- `\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` predicate returned an invalid value of type ${(typeof(
436
- result,
437
- ) :> string)}. Expected boolean or a filter object.`,
438
- )
439
- }
440
-
441
- if shouldRegister {
442
- matchedAny := true
443
- HandlerRegister.registerOnBlock(
444
- ~name,
445
- ~chainId,
446
- ~interval=range._every,
447
- ~startBlock=range._gte,
448
- ~endBlock=range._lte,
449
- ~handler=typedHandler,
450
- )
451
- }
452
- })
453
-
454
- // Catches misconfigured `where` predicates that return `false` for every
455
- // configured chain — the handler would otherwise never fire with no hint.
456
- // Includes the ecosystem-specific method name so SVM users see "onSlot"
457
- // and don't get confused looking for a "Block handler" they never wrote.
458
- if !matchedAny.contents {
459
- logger->Logging.childWarn(
460
- `\`indexer.${ecosystem.onBlockMethodName}\` matched 0 chains. Check the \`where\` predicate.`,
461
- )
462
- }
341
+ HandlerRegister.registerOnBlock(
342
+ ~name=raw["name"],
343
+ ~where=raw["where"],
344
+ ~handler=handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>),
345
+ ~getChainsObject=config => {
346
+ let (chains, _) = buildChainsObject(~config)
347
+ chains->(Utils.magic: {..} => dict<unknown>)
348
+ },
349
+ )
463
350
  }
464
351
 
465
352
  // Ecosystem-specific surface: EVM/Fuel expose event + block handlers; SVM
@@ -506,8 +393,7 @@ let getGlobalIndexer = (): 'indexer => {
506
393
  let get = (~prop: string) =>
507
394
  switch prop {
508
395
  | "name" => Config.load().name->(Utils.magic: string => unknown)
509
- | "description" =>
510
- Config.load().description->(Utils.magic: option<string> => unknown)
396
+ | "description" => Config.load().description->(Utils.magic: option<string> => unknown)
511
397
  | "chainIds" => {
512
398
  let (_, chainIds) = buildChainsObject(~config=Config.load())
513
399
  chainIds->(Utils.magic: array<int> => unknown)
@@ -616,7 +502,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
616
502
  app->get("/metrics", (_req, res) => {
617
503
  res->set("Content-Type", PromClient.defaultRegister->PromClient.getContentType)
618
504
  let _ =
619
- Metrics.collect(~state=indexerStateRef.contents)->Promise.thenResolve(metrics =>
505
+ Metrics.collect(~state=getIndexerState())->Promise.thenResolve(metrics =>
620
506
  res->endWithData(metrics)
621
507
  )
622
508
  })
@@ -709,7 +595,7 @@ let start = async (
709
595
  | Some(p) => p
710
596
  | None => PgStorage.makePersistenceFromConfig(~config)
711
597
  }
712
- globalPersistenceRef := Some(persistence)
598
+ setGlobalPersistence(persistence)
713
599
  await persistence->Persistence.init(
714
600
  ~reset,
715
601
  ~chainConfigs=config.chainMap->ChainMap.values,
@@ -756,7 +642,7 @@ let start = async (
756
642
 
757
643
  if !isTest {
758
644
  startServer(~persistence, ~isDevelopmentMode, ~getState=() =>
759
- switch indexerStateRef.contents {
645
+ switch getIndexerState() {
760
646
  | None => Initializing({})
761
647
  | Some(state) => {
762
648
  let chains =
@@ -786,7 +672,7 @@ let start = async (
786
672
  if shouldUseTui {
787
673
  let _rerender = Tui.start(~getState=() => state)
788
674
  }
789
- indexerStateRef := Some(state)
675
+ setIndexerState(state)
790
676
  state->IndexerLoop.start
791
677
  await runUntilFatalError
792
678
  }