envio 3.15.0 → 3.16.0-subgraph

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 (186) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +11 -2
  3. package/fuel.schema.json +10 -1
  4. package/index.d.ts +6 -0
  5. package/licenses/README.md +1 -1
  6. package/package.json +10 -9
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/Bin.res +7 -0
  11. package/src/Bin.res.mjs +8 -0
  12. package/src/ChainFetching.res +450 -16
  13. package/src/ChainFetching.res.mjs +403 -32
  14. package/src/ChainState.res +124 -7
  15. package/src/ChainState.res.mjs +128 -67
  16. package/src/ChainState.resi +26 -0
  17. package/src/Clock.res +57 -0
  18. package/src/Clock.res.mjs +72 -0
  19. package/src/Config.res +31 -3
  20. package/src/Config.res.mjs +15 -4
  21. package/src/ContractRegisterContext.res +3 -6
  22. package/src/ContractRegisterContext.res.mjs +4 -2
  23. package/src/Core.res +95 -36
  24. package/src/Core.res.mjs +90 -1
  25. package/src/CrossChainState.res +12 -2
  26. package/src/CrossChainState.res.mjs +3 -3
  27. package/src/Ecosystem.res +7 -4
  28. package/src/Ecosystem.res.mjs +8 -8
  29. package/src/EffectCacheLookup.res +98 -0
  30. package/src/EffectCacheLookup.res.mjs +116 -0
  31. package/src/EffectState.res +10 -2
  32. package/src/EffectState.res.mjs +8 -4
  33. package/src/EffectState.resi +3 -1
  34. package/src/EntityId.res +2 -7
  35. package/src/EntityId.res.mjs +1 -8
  36. package/src/EntityTables.res +4 -1
  37. package/src/EntityTables.res.mjs +1 -1
  38. package/src/Env.res +20 -0
  39. package/src/Env.res.mjs +16 -0
  40. package/src/EventProcessing.res +38 -1
  41. package/src/EventProcessing.res.mjs +28 -1
  42. package/src/FetchState.res +408 -73
  43. package/src/FetchState.res.mjs +361 -91
  44. package/src/FinalizeBackfill.res +2 -4
  45. package/src/FinalizeBackfill.res.mjs +3 -4
  46. package/src/HandlerLoader.res +39 -15
  47. package/src/HandlerLoader.res.mjs +27 -8
  48. package/src/HandlerRegister.res +4 -0
  49. package/src/HandlerRegister.res.mjs +5 -2
  50. package/src/HandlerRegister.resi +2 -0
  51. package/src/InMemoryTable.res +17 -16
  52. package/src/InMemoryTable.res.mjs +14 -15
  53. package/src/IndexerState.res +28 -4
  54. package/src/IndexerState.res.mjs +19 -6
  55. package/src/IndexerState.resi +4 -0
  56. package/src/Internal.res +61 -49
  57. package/src/Internal.res.mjs +36 -80
  58. package/src/LoadLayer.res +54 -15
  59. package/src/LoadLayer.res.mjs +75 -45
  60. package/src/LoadLayer.resi +26 -0
  61. package/src/Main.res +14 -1
  62. package/src/Main.res.mjs +4 -3
  63. package/src/Persistence.res +3 -23
  64. package/src/Persistence.res.mjs +3 -3
  65. package/src/PreloadLookahead.res +202 -0
  66. package/src/PreloadLookahead.res.mjs +218 -0
  67. package/src/Rollback.res +0 -1
  68. package/src/Rollback.res.mjs +1 -1
  69. package/src/RunScope.res +3 -0
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +1 -1
  72. package/src/SimulateItems.res +42 -26
  73. package/src/SimulateItems.res.mjs +40 -16
  74. package/src/StagedRows.res +106 -0
  75. package/src/StagedRows.res.mjs +102 -0
  76. package/src/Staging.res +32 -14
  77. package/src/Staging.res.mjs +17 -20
  78. package/src/Storage.res +303 -948
  79. package/src/Storage.res.mjs +229 -708
  80. package/src/SubgraphLog.res +19 -0
  81. package/src/SubgraphLog.res.mjs +26 -0
  82. package/src/Supervisor.res +75 -56
  83. package/src/Supervisor.res.mjs +55 -26
  84. package/src/SyncWrites.res +107 -0
  85. package/src/SyncWrites.res.mjs +98 -0
  86. package/src/TestIndexer.res +22 -27
  87. package/src/TestIndexer.res.mjs +20 -17
  88. package/src/Throttler.res +15 -8
  89. package/src/Throttler.res.mjs +9 -6
  90. package/src/Throttler.resi +1 -1
  91. package/src/UserContext.res +419 -46
  92. package/src/UserContext.res.mjs +318 -30
  93. package/src/Utils.res +17 -6
  94. package/src/Utils.res.mjs +12 -6
  95. package/src/Writing.res +0 -1
  96. package/src/Writing.res.mjs +1 -1
  97. package/src/bindings/ClickHouse.res +0 -322
  98. package/src/bindings/ClickHouse.res.mjs +0 -338
  99. package/src/bindings/StorageClient.res +108 -174
  100. package/src/bindings/StorageClient.res.mjs +19 -8
  101. package/src/bindings/Vitest.res +0 -26
  102. package/src/bindings/Vitest.res.mjs +0 -3
  103. package/src/db/Table.res +24 -150
  104. package/src/db/Table.res.mjs +38 -272
  105. package/src/sources/AddressSet.res +2 -0
  106. package/src/sources/AddressStore.res +3 -0
  107. package/src/sources/CallSource.res +274 -0
  108. package/src/sources/CallSource.res.mjs +196 -0
  109. package/src/sources/ChainSources.res +2 -0
  110. package/src/sources/ChainSources.res.mjs +2 -2
  111. package/src/sources/EvmChain.res +10 -1
  112. package/src/sources/EvmChain.res.mjs +6 -3
  113. package/src/sources/EvmHyperSyncSource.res +2 -2
  114. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  115. package/src/sources/EvmRpcClient.res +13 -3
  116. package/src/sources/EvmRpcClient.res.mjs +4 -3
  117. package/src/sources/EvmRpcWs.res +4 -3
  118. package/src/sources/EvmRpcWs.res.mjs +2 -2
  119. package/src/sources/HeightFeed.res +31 -10
  120. package/src/sources/HeightFeed.res.mjs +10 -6
  121. package/src/sources/HeightStream.res +32 -17
  122. package/src/sources/HeightStream.res.mjs +10 -10
  123. package/src/sources/HyperSync.res +2 -1
  124. package/src/sources/HyperSync.res.mjs +2 -1
  125. package/src/sources/HyperSyncSSE.res +4 -6
  126. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  127. package/src/sources/RpcSource.res +10 -1
  128. package/src/sources/RpcSource.res.mjs +4 -3
  129. package/src/sources/Source.res +3 -0
  130. package/src/sources/SourceManager.res +84 -77
  131. package/src/sources/SourceManager.res.mjs +17 -20
  132. package/src/sources/SourceManager.resi +3 -4
  133. package/src/sources/StartBlockResolver.res +1 -0
  134. package/src/sources/StartBlockResolver.res.mjs +3 -2
  135. package/src/sources/SvmHyperSyncSource.res +2 -2
  136. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  137. package/src/subgraph/abi-types.ts +84 -0
  138. package/src/subgraph/aggregations.ts +745 -0
  139. package/src/subgraph/as-package.ts +143 -0
  140. package/src/subgraph/assemblyscript-regex.ts +67 -0
  141. package/src/subgraph/assemblyscript.ts +731 -0
  142. package/src/subgraph/blocks.ts +398 -0
  143. package/src/subgraph/calls-source.ts +702 -0
  144. package/src/subgraph/calls.ts +258 -0
  145. package/src/subgraph/conformance.ts +106 -0
  146. package/src/subgraph/errors.ts +85 -0
  147. package/src/subgraph/graft.ts +73 -0
  148. package/src/subgraph/graph-cli-worker.mjs +35 -0
  149. package/src/subgraph/graph-cli.ts +900 -0
  150. package/src/subgraph/graph-node-decimal.ts +199 -0
  151. package/src/subgraph/graph-ts-enums.ts +28 -0
  152. package/src/subgraph/graph-ts-types/VERSION +2 -0
  153. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  154. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  155. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  156. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  157. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  158. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  159. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  160. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  161. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  162. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  163. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  164. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  165. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  166. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  167. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  168. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  169. package/src/subgraph/graph-ts.ts +4339 -0
  170. package/src/subgraph/hosts.ts +554 -0
  171. package/src/subgraph/log.ts +28 -0
  172. package/src/subgraph/receipts.ts +184 -0
  173. package/src/subgraph/register-slots.ts +93 -0
  174. package/src/subgraph/rollup.ts +109 -0
  175. package/src/subgraph/rpc.ts +337 -0
  176. package/src/subgraph/runtime.ts +2059 -0
  177. package/src/subgraph/scope.ts +176 -0
  178. package/src/subgraph/stats.ts +42 -0
  179. package/src/subgraph/template-instances.ts +66 -0
  180. package/src/subgraph/toolchain.ts +327 -0
  181. package/src/subgraph/transient.ts +176 -0
  182. package/svm.schema.json +11 -2
  183. package/src/PgWriting.res +0 -36
  184. package/src/PgWriting.res.mjs +0 -34
  185. package/src/db/IndexDefinition.res +0 -47
  186. package/src/db/IndexDefinition.res.mjs +0 -67
package/src/Internal.res CHANGED
@@ -724,6 +724,12 @@ type onBlockArgs = {
724
724
  context: handlerContext,
725
725
  }
726
726
 
727
+ type onBlockContractRegisterArgs = {
728
+ slot?: int,
729
+ block?: eventBlock,
730
+ context: contractRegisterContext,
731
+ }
732
+
727
733
  type onBlockRegistration = {
728
734
  // When there are multiple onBlock handlers per chain,
729
735
  // we want to use the order they are defined for sorting
@@ -738,6 +744,14 @@ type onBlockRegistration = {
738
744
  // `None` when the handler reads only the block's key, which needs nothing
739
745
  // from a source: its items are generated locally, block by block.
740
746
  blockFieldMask: option<float>,
747
+ // Subgraph mode only: a `once` block handler, whose trigger graph-node runs
748
+ // ahead of everything else in its block (`EthereumBlockTriggerType::Start`).
749
+ runsFirst?: bool,
750
+ // Subgraph mode only: the register pass of a block handler that can create a
751
+ // data source. It runs for each of the handler's blocks at fetch time, and no
752
+ // block item is processed until every pass up to its block has run and what
753
+ // they registered is fetched, as an event's contractRegister.
754
+ contractRegister?: onBlockContractRegisterArgs => promise<unit>,
741
755
  }
742
756
 
743
757
  @tag("kind")
@@ -785,6 +799,53 @@ external getItemOrderPath: item => Nullable.t<array<int>> = "orderPath"
785
799
  // block ahead of that block's handlers without a `switch`.
786
800
  @get
787
801
  external getItemKind: item => int = "kind"
802
+ // Set only in subgraph mode, on an event of an address registered in the very
803
+ // block it was logged in: graph-node runs a new data source's creation-block
804
+ // events after everything else in that block. The value is the round, as
805
+ // graph-node counts them: 1 for a data source a block's own triggers created,
806
+ // 2 for one that round-1 handlers created, and so on.
807
+ @get
808
+ external getItemCreationRound: item => option<int> = "runsAfterBlock"
809
+ @set
810
+ external markItemCreationRound: (item, int) => unit = "runsAfterBlock"
811
+
812
+ let itemRunsFirst = (item: item) =>
813
+ switch item {
814
+ | Block({onBlockRegistration: {runsFirst: true}}) => true
815
+ | _ => false
816
+ }
817
+
818
+ // Subgraph mode only. A call trigger travels through fetch and batching as an
819
+ // event item (see `subgraph/calls-source.ts`), and graph-node orders it by its
820
+ // transaction among the logs, so its item carries a rank: 1 for a call
821
+ // handler's, 2 for the `filter: call` block handler's. Absent on every log.
822
+ @get
823
+ external getItemCallRank: item => option<int> = "callRank"
824
+ @set
825
+ external setItemCallRank: (item, int) => unit = "callRank"
826
+ @get
827
+ external getItemTransactionIndex: item => int = "transactionIndex"
828
+
829
+ // Flips on when the first call item is built, so a run without call triggers
830
+ // compares items exactly as before.
831
+ let callOrderingActive = ref(false)
832
+
833
+ // The call trigger an event of this name is, if it is one: the subgraph
834
+ // translator names them `__envio_call_<selector>_<function>`, and the block
835
+ // handler's `__envio_call_any`. No log carries either name.
836
+ let callRankOfEventName = (name: string): option<int> =>
837
+ if name->String.startsWith("__envio_call_any") {
838
+ Some(2)
839
+ } else if name->String.startsWith("__envio_call_") {
840
+ Some(1)
841
+ } else {
842
+ None
843
+ }
844
+
845
+ let markItemCall = (item, ~rank) => {
846
+ item->setItemCallRank(rank)
847
+ callOrderingActive := true
848
+ }
788
849
 
789
850
  let getItemChainId = item =>
790
851
  switch item {
@@ -950,55 +1011,6 @@ module EffectCache = {
950
1011
  | CrossChain => cacheTablePrefix ++ effectName
951
1012
  | Chain(chainId) => `envio_${chainId->ChainId.toString}_effect_${effectName}`
952
1013
  }
953
-
954
- // Only accepts a canonical decimal chain id ("7", not "007" or "1foo") —
955
- // the schema's parser follows parseFloat semantics and accepts both.
956
- let parseChainId = str =>
957
- switch try Some(str->ChainId.normalizeOrThrow) catch {
958
- | _ => None
959
- } {
960
- | Some(chainId) if chainId->ChainId.toString === str => Some(chainId)
961
- | _ => None
962
- }
963
-
964
- let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/
965
- let crossChainRe = /^envio_effect_(.+)$/
966
-
967
- // Inverse of toTableName. Returns None for any table name that isn't a cache
968
- // table. Chain-scoped is tried first: the `_effect_` separator keeps effect
969
- // names that themselves start with digits unambiguous.
970
- let fromTableName = (tableName): option<(string, chainScope)> =>
971
- switch RegExp.exec(chainScopedRe, tableName) {
972
- | Some(result) =>
973
- switch (
974
- RegExp.Result.matches(result)->Array.get(0),
975
- RegExp.Result.matches(result)->Array.get(1),
976
- ) {
977
- | (Some(Some(chainIdStr)), Some(Some(effectName))) =>
978
- switch parseChainId(chainIdStr) {
979
- | Some(chainId) => Some((effectName, Chain(chainId)))
980
- | None => None
981
- }
982
- | _ => None
983
- }
984
- | None =>
985
- switch RegExp.exec(crossChainRe, tableName) {
986
- | Some(result) =>
987
- switch RegExp.Result.matches(result)->Array.get(0) {
988
- | Some(Some(effectName)) => Some((effectName, CrossChain))
989
- | _ => None
990
- }
991
- | None => None
992
- }
993
- }
994
-
995
- // Relative posix path within .envio/cache. Chain-scoped caches live one
996
- // directory level deep, named by chain id.
997
- let toCachePath = (~effectName, ~scope) =>
998
- switch scope {
999
- | CrossChain => effectName ++ ".tsv"
1000
- | Chain(chainId) => `${chainId->ChainId.toString}/${effectName}.tsv`
1001
- }
1002
1014
  }
1003
1015
 
1004
1016
  let cacheOutputSchema = S.json(~validate=false)->(Utils.magic: S.t<JSON.t> => S.t<effectOutput>)
@@ -188,6 +188,37 @@ function eventItemFieldsJson(eventItem, address) {
188
188
  return `{` + contractAndEventJson(eventItem.onEventRegistration.eventConfig) + `,"block":` + eventItem.blockNumber.toString() + `,"logIndex":` + eventItem.logIndex.toString() + `,"address":` + JSON.stringify(address) + `}`;
189
189
  }
190
190
 
191
+ function itemRunsFirst(item) {
192
+ if (item.kind === 0) {
193
+ return false;
194
+ }
195
+ let match = item.onBlockRegistration.runsFirst;
196
+ if (match !== undefined) {
197
+ return match;
198
+ } else {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ let callOrderingActive = {
204
+ contents: false
205
+ };
206
+
207
+ function callRankOfEventName(name) {
208
+ if (name.startsWith("__envio_call_any")) {
209
+ return 2;
210
+ } else if (name.startsWith("__envio_call_")) {
211
+ return 1;
212
+ } else {
213
+ return;
214
+ }
215
+ }
216
+
217
+ function markItemCall(item, rank) {
218
+ item.callRank = rank;
219
+ callOrderingActive.contents = true;
220
+ }
221
+
191
222
  function getItemChainId(item) {
192
223
  if (item.kind === 0) {
193
224
  return item.chainId;
@@ -240,87 +271,8 @@ function toTableName(effectName, scope) {
240
271
  }
241
272
  }
242
273
 
243
- function parseChainId(str) {
244
- let chainId;
245
- try {
246
- chainId = Primitive_option.some(ChainId.normalizeOrThrow(str));
247
- } catch (exn) {
248
- chainId = undefined;
249
- }
250
- if (chainId === undefined) {
251
- return;
252
- }
253
- let chainId$1 = Primitive_option.valFromOption(chainId);
254
- if (ChainId.toString(chainId$1) === str) {
255
- return Primitive_option.some(chainId$1);
256
- }
257
- }
258
-
259
- let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/;
260
-
261
- let crossChainRe = /^envio_effect_(.+)$/;
262
-
263
- function fromTableName(tableName) {
264
- let result = chainScopedRe.exec(tableName);
265
- if (result == null) {
266
- let result$1 = crossChainRe.exec(tableName);
267
- if (result$1 == null) {
268
- return;
269
- }
270
- let match = result$1.slice(1)[0];
271
- if (match === undefined) {
272
- return;
273
- }
274
- let effectName = Primitive_option.valFromOption(match);
275
- if (effectName !== undefined) {
276
- return [
277
- effectName,
278
- "crossChain"
279
- ];
280
- } else {
281
- return;
282
- }
283
- }
284
- let match$1 = result.slice(1)[0];
285
- let match$2 = result.slice(1)[1];
286
- if (match$1 === undefined) {
287
- return;
288
- }
289
- let chainIdStr = Primitive_option.valFromOption(match$1);
290
- if (chainIdStr === undefined) {
291
- return;
292
- }
293
- if (match$2 === undefined) {
294
- return;
295
- }
296
- let effectName$1 = Primitive_option.valFromOption(match$2);
297
- if (effectName$1 === undefined) {
298
- return;
299
- }
300
- let chainId = parseChainId(chainIdStr);
301
- if (chainId !== undefined) {
302
- return [
303
- effectName$1,
304
- Primitive_option.valFromOption(chainId)
305
- ];
306
- }
307
- }
308
-
309
- function toCachePath(effectName, scope) {
310
- if (scope === "crossChain") {
311
- return effectName + ".tsv";
312
- } else {
313
- return ChainId.toString(scope) + `/` + effectName + `.tsv`;
314
- }
315
- }
316
-
317
274
  let EffectCache = {
318
- toTableName: toTableName,
319
- parseChainId: parseChainId,
320
- chainScopedRe: chainScopedRe,
321
- crossChainRe: crossChainRe,
322
- fromTableName: fromTableName,
323
- toCachePath: toCachePath
275
+ toTableName: toTableName
324
276
  };
325
277
 
326
278
  let cacheOutputSchema = S$RescriptSchema.json(false);
@@ -356,6 +308,10 @@ export {
356
308
  dependsOnAddresses,
357
309
  svmAccountSlotName,
358
310
  eventItemFieldsJson,
311
+ itemRunsFirst,
312
+ callOrderingActive,
313
+ callRankOfEventName,
314
+ markItemCall,
359
315
  getItemChainId,
360
316
  fuelSupplyParamsSchema,
361
317
  fuelTransferParamsSchema,
package/src/LoadLayer.res CHANGED
@@ -16,7 +16,12 @@ let loadById = (
16
16
  ~entityId,
17
17
  ) => {
18
18
  let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope)
19
- switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(entityId) {
19
+ // The ids here are typed as strings but hold whatever scalar the id is.
20
+ let entityIdToKey =
21
+ inMemTable.entityIdToKey->(Utils.magic: (EntityId.t => string) => string => string)
22
+ switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(
23
+ entityIdToKey(entityId),
24
+ ) {
20
25
  // A row already in memory is final, so even a grouped call skips the load
21
26
  // manager: grouping it would only resolve it a tick later to the same value.
22
27
  | Some(change) => Promise.resolve(change->InMemoryTable.Entity.mapChangeToEntity)
@@ -53,14 +58,14 @@ let loadById = (
53
58
  //Set the entity in the in memory store
54
59
  for idx in 0 to dbEntities->Array.length - 1 {
55
60
  let entity = dbEntities->Array.getUnsafe(idx)
56
- entitiesMap->Dict.set(entity.id, entity)
61
+ entitiesMap->Dict.set(entityIdToKey(entity.id), entity)
57
62
  }
58
63
  let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
59
64
  idsToLoad->Array.forEach(entityId => {
60
65
  inMemTable->InMemoryTable.Entity.initValue(
61
66
  ~committedCheckpointId,
62
- ~key=entityId,
63
- ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
67
+ ~entityId=entityId->EntityId.unsafeOfString,
68
+ ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityIdToKey(entityId)),
64
69
  )
65
70
  })
66
71
 
@@ -77,7 +82,7 @@ let loadById = (
77
82
  ~key,
78
83
  ~load,
79
84
  ~shouldGroup,
80
- ~hasher=LoadManager.noopHasher,
85
+ ~hasher=entityIdToKey,
81
86
  ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafe,
82
87
  ~hasInMemory=hash => inMemTable.latestEntityChangeById->Dict.has(hash),
83
88
  ~input=entityId,
@@ -140,7 +145,7 @@ let rec executeWithRateLimit = (
140
145
  }
141
146
 
142
147
  | Some(state) =>
143
- let now = Date.now()
148
+ let now = state.clock.now()
144
149
 
145
150
  // Check if we need to reset the window
146
151
  if now >= state.windowStartTime +. state.durationMs->Int.toFloat {
@@ -188,7 +193,8 @@ let rec executeWithRateLimit = (
188
193
  | None =>
189
194
  millisUntilReset :=
190
195
  (state.windowStartTime +. state.durationMs->Int.toFloat -. now)->Float.toInt
191
- let p = Utils.delay(millisUntilReset.contents)
196
+ let p =
197
+ state.clock->Clock.delay(~label="effect rate limit window", millisUntilReset.contents)
192
198
  state.nextWindowPromise = Some(p)
193
199
  p
194
200
  }
@@ -335,6 +341,45 @@ let loadEffect = (
335
341
  )
336
342
  }
337
343
 
344
+ // Keying an _in walks every value, so it's computed once and handed on rather
345
+ // than recomputed by each reader. Before answering "not in memory", the rows
346
+ // already loaded for other filters are checked: they may cover this one.
347
+ let indexFilter = (inMemTable, ~filter, ~table) => {
348
+ let filterKey = filter->EntityFilter.toString(~table)
349
+ if !(inMemTable->InMemoryTable.Entity.hasIndex)(filterKey) {
350
+ inMemTable->InMemoryTable.Entity.tryIndexFromLoadedValues(~filter, ~table)
351
+ }
352
+ filterKey
353
+ }
354
+
355
+ // A recorded absence is an answer too: `Some(None)` means the entity is known
356
+ // not to exist, so no load is needed to say so.
357
+ let getByIdInMemory = (
358
+ ~entityConfig: Internal.entityConfig,
359
+ ~scope,
360
+ ~indexerState,
361
+ ~entityId: string,
362
+ ) => {
363
+ let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope)
364
+ let entityKey = inMemTable.entityIdToKey(entityId->EntityId.unsafeOfString)
365
+ inMemTable.latestEntityChangeById->Dict.has(entityKey)
366
+ ? Some((inMemTable->InMemoryTable.Entity.getUnsafe)(entityKey))
367
+ : None
368
+ }
369
+
370
+ let getByFilterInMemory = (
371
+ ~entityConfig: Internal.entityConfig,
372
+ ~scope,
373
+ ~indexerState,
374
+ ~filter: EntityFilter.t,
375
+ ) => {
376
+ let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope)
377
+ let filterKey = inMemTable->indexFilter(~filter, ~table=entityConfig.table)
378
+ (inMemTable->InMemoryTable.Entity.hasIndex)(filterKey)
379
+ ? Some((inMemTable->InMemoryTable.Entity.getUnsafeOnIndex)(filterKey))
380
+ : None
381
+ }
382
+
338
383
  let loadByParsedFilter = (
339
384
  ~loadManager,
340
385
  ~persistence: Persistence.t,
@@ -387,7 +432,7 @@ let loadByParsedFilter = (
387
432
  entities->Array.forEach(entity => {
388
433
  inMemTable->InMemoryTable.Entity.initValue(
389
434
  ~committedCheckpointId,
390
- ~key=entity.id,
435
+ ~entityId=entity.id->EntityId.unsafeOfString,
391
436
  ~entity=Some(entity),
392
437
  )
393
438
  })
@@ -425,13 +470,7 @@ let loadByParsedFilter = (
425
470
  )
426
471
  }
427
472
 
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
- }
473
+ let filterKey = inMemTable->indexFilter(~filter, ~table=entityConfig.table)
435
474
 
436
475
  loadManager->LoadManager.call(
437
476
  ~key,
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Clock from "./Clock.res.mjs";
3
4
  import * as Utils from "./Utils.res.mjs";
4
5
  import * as Logger from "./Logger.res.mjs";
5
6
  import * as ChainId from "./ChainId.res.mjs";
@@ -14,6 +15,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
14
15
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
15
16
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
16
17
  import * as InMemoryTable from "./InMemoryTable.res.mjs";
18
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
17
19
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
18
20
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
19
21
 
@@ -27,7 +29,8 @@ function scopeKeySuffix(scope) {
27
29
 
28
30
  function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, entityId) {
29
31
  let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
30
- let change = inMemTable.latestEntityChangeById[entityId];
32
+ let entityIdToKey = inMemTable.entityIdToKey;
33
+ let change = inMemTable.latestEntityChangeById[entityIdToKey(entityId)];
31
34
  if (change !== undefined) {
32
35
  return Promise.resolve(InMemoryTable.Entity.mapChangeToEntity(change));
33
36
  }
@@ -50,13 +53,13 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
50
53
  let entitiesMap = {};
51
54
  for (let idx = 0, idx_finish = dbEntities.length; idx < idx_finish; ++idx) {
52
55
  let entity = dbEntities[idx];
53
- entitiesMap[entity.id] = entity;
56
+ entitiesMap[entityIdToKey(entity.id)] = entity;
54
57
  }
55
58
  let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
56
- idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityId]));
59
+ idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityIdToKey(entityId)]));
57
60
  return IndexerState.endStorageLoad(indexerState, timerRef, storageName, key, idsToLoad.length, dbEntities.length);
58
61
  };
59
- return LoadManager.call(loadManager, entityId, key, load, LoadManager.noopHasher, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
62
+ return LoadManager.call(loadManager, entityId, key, load, entityIdToKey, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
60
63
  }
61
64
 
62
65
  function callEffect(effect, arg, inMemTable, timerRef, onError) {
@@ -69,7 +72,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
69
72
  let promises = [];
70
73
  let state = inMemTable.rateLimitState;
71
74
  if (state !== undefined) {
72
- let now = Date.now();
75
+ let now = state.clock.now();
73
76
  if (now >= state.windowStartTime + state.durationMs) {
74
77
  state.availableCalls = state.callsPerDuration;
75
78
  state.windowStartTime = now;
@@ -98,7 +101,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
98
101
  nextWindowPromise = p;
99
102
  } else {
100
103
  millisUntilReset.contents = state.windowStartTime + state.durationMs - now | 0;
101
- let p$1 = Utils.delay(millisUntilReset.contents);
104
+ let p$1 = Clock.delay(state.clock, "effect rate limit window", millisUntilReset.contents);
102
105
  state.nextWindowPromise = p$1;
103
106
  nextWindowPromise = p$1;
104
107
  }
@@ -184,47 +187,71 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
184
187
  return LoadManager.call(loadManager, effectArgs, key, load, args => args.cacheKey, shouldGroup, hash => InMemoryStore.hasEffectOutput(inMemTable, hash), hash => InMemoryStore.getEffectOutputUnsafe(inMemTable, hash));
185
188
  }
186
189
 
190
+ function indexFilter(inMemTable, filter, table) {
191
+ let filterKey = EntityFilter.toString(filter, table);
192
+ if (!InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
193
+ InMemoryTable.Entity.tryIndexFromLoadedValues(inMemTable, filter, table);
194
+ }
195
+ return filterKey;
196
+ }
197
+
198
+ function getByIdInMemory(entityConfig, scope, indexerState, entityId) {
199
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
200
+ let entityKey = inMemTable.entityIdToKey(entityId);
201
+ if (entityKey in inMemTable.latestEntityChangeById) {
202
+ return Primitive_option.some(InMemoryTable.Entity.getUnsafe(inMemTable)(entityKey));
203
+ }
204
+ }
205
+
206
+ function getByFilterInMemory(entityConfig, scope, indexerState, filter) {
207
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
208
+ let filterKey = indexFilter(inMemTable, filter, entityConfig.table);
209
+ if (InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
210
+ return InMemoryTable.Entity.getUnsafeOnIndex(inMemTable)(filterKey);
211
+ }
212
+ }
213
+
214
+ function loadByParsedFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, filter) {
215
+ let key = EntityFilter.toOperationKey(filter, entityConfig.name) + scopeKeySuffix(scope);
216
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
217
+ let load = async (filters, param) => {
218
+ let storage = Persistence.getInitializedStorageOrThrow(persistence);
219
+ let storageName = storage.entityStoreName(entityConfig);
220
+ let timerRef = IndexerState.startStorageLoad(indexerState, storageName, key);
221
+ let size = {
222
+ contents: 0
223
+ };
224
+ filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter, entityConfig.table));
225
+ await storage.ensureQueryIndexes(entityConfig, scope, filters);
226
+ let queries = EntityFilter.merge(filters);
227
+ await Promise.all(queries.map(async filter => {
228
+ try {
229
+ let entities = await storage.loadOrThrow(EntityFilter.scoped(filter, entityConfig.table, scope), entityConfig.table);
230
+ let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
231
+ entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
232
+ InMemoryTable.Entity.recordLoadedValues(inMemTable, filter, entityConfig.table);
233
+ size.contents = size.contents + entities.length | 0;
234
+ return;
235
+ } catch (raw_exn) {
236
+ let exn = Primitive_exceptions.internalToException(raw_exn);
237
+ if (exn.RE_EXN_ID === Persistence.StorageError) {
238
+ return ErrorHandling.mkLogAndRaise(Logger.withFields(IndexerState.loggerForScope(indexerState, scope), {
239
+ operation: key,
240
+ params: EntityFilter.getParams(filter)
241
+ }), exn.message, exn.reason);
242
+ }
243
+ throw exn;
244
+ }
245
+ }));
246
+ return IndexerState.endStorageLoad(indexerState, timerRef, storageName, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
247
+ };
248
+ let filterKey = indexFilter(inMemTable, filter, entityConfig.table);
249
+ return LoadManager.call(loadManager, filter, key, load, param => filterKey, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
250
+ }
251
+
187
252
  function loadByFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, filter) {
188
253
  try {
189
- let filter$1 = EntityFilter.parseOrThrow(filter, entityConfig.name, entityConfig.table);
190
- let key = EntityFilter.toOperationKey(filter$1, entityConfig.name) + scopeKeySuffix(scope);
191
- let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
192
- let load = async (filters, param) => {
193
- let storage = Persistence.getInitializedStorageOrThrow(persistence);
194
- let storageName = storage.entityStoreName(entityConfig);
195
- let timerRef = IndexerState.startStorageLoad(indexerState, storageName, key);
196
- let size = {
197
- contents: 0
198
- };
199
- filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter, entityConfig.table));
200
- await storage.ensureQueryIndexes(entityConfig, scope, filters);
201
- let queries = EntityFilter.merge(filters);
202
- await Promise.all(queries.map(async filter => {
203
- try {
204
- let entities = await storage.loadOrThrow(EntityFilter.scoped(filter, entityConfig.table, scope), entityConfig.table);
205
- let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
206
- entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
207
- InMemoryTable.Entity.recordLoadedValues(inMemTable, filter, entityConfig.table);
208
- size.contents = size.contents + entities.length | 0;
209
- return;
210
- } catch (raw_exn) {
211
- let exn = Primitive_exceptions.internalToException(raw_exn);
212
- if (exn.RE_EXN_ID === Persistence.StorageError) {
213
- return ErrorHandling.mkLogAndRaise(Logger.withFields(IndexerState.loggerForScope(indexerState, scope), {
214
- operation: key,
215
- params: EntityFilter.getParams(filter)
216
- }), exn.message, exn.reason);
217
- }
218
- throw exn;
219
- }
220
- }));
221
- return IndexerState.endStorageLoad(indexerState, timerRef, storageName, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
222
- };
223
- let filterKey = EntityFilter.toString(filter$1, entityConfig.table);
224
- if (!InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
225
- InMemoryTable.Entity.tryIndexFromLoadedValues(inMemTable, filter$1, entityConfig.table);
226
- }
227
- return LoadManager.call(loadManager, filter$1, key, load, param => filterKey, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
254
+ return loadByParsedFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, EntityFilter.parseOrThrow(filter, entityConfig.name, entityConfig.table));
228
255
  } catch (raw_exn) {
229
256
  let exn = Primitive_exceptions.internalToException(raw_exn);
230
257
  return Promise.reject(Utils.prettifyExn(exn));
@@ -234,6 +261,9 @@ function loadByFilter(loadManager, persistence, entityConfig, scope, indexerStat
234
261
  export {
235
262
  loadById,
236
263
  loadByFilter,
264
+ loadByParsedFilter,
265
+ getByIdInMemory,
266
+ getByFilterInMemory,
237
267
  loadEffect,
238
268
  }
239
269
  /* Utils Not a pure module */
@@ -18,6 +18,32 @@ let loadByFilter: (
18
18
  ~filter: dict<dict<unknown>>,
19
19
  ) => promise<array<Internal.entity>>
20
20
 
21
+ let loadByParsedFilter: (
22
+ ~loadManager: LoadManager.t,
23
+ ~persistence: Persistence.t,
24
+ ~entityConfig: Internal.entityConfig,
25
+ ~scope: Internal.chainScope,
26
+ ~indexerState: IndexerState.t,
27
+ ~shouldGroup: bool,
28
+ ~filter: EntityFilter.t,
29
+ ) => promise<array<Internal.entity>>
30
+
31
+ // What a get or getWhere answers without a load, or None when it would need
32
+ // one. The sync context reads through these and suspends on None.
33
+ let getByIdInMemory: (
34
+ ~entityConfig: Internal.entityConfig,
35
+ ~scope: Internal.chainScope,
36
+ ~indexerState: IndexerState.t,
37
+ ~entityId: string,
38
+ ) => option<option<Internal.entity>>
39
+
40
+ let getByFilterInMemory: (
41
+ ~entityConfig: Internal.entityConfig,
42
+ ~scope: Internal.chainScope,
43
+ ~indexerState: IndexerState.t,
44
+ ~filter: EntityFilter.t,
45
+ ) => option<array<Internal.entity>>
46
+
21
47
  let loadEffect: (
22
48
  ~loadManager: LoadManager.t,
23
49
  ~persistence: Persistence.t,
package/src/Main.res CHANGED
@@ -303,12 +303,25 @@ let getGlobalIndexer = (): 'indexer => {
303
303
 
304
304
  let onBlockFn = (rawOptions: 'a, handler: 'b) => {
305
305
  HandlerRegister.throwIfFinishedRegistration(~methodName="onBlock")
306
- let raw = rawOptions->(Utils.magic: 'a => {"name": string, "where": unknown, "fields": unknown})
306
+ let raw =
307
+ rawOptions->(
308
+ Utils.magic: 'a => {
309
+ "name": string,
310
+ "where": unknown,
311
+ "fields": unknown,
312
+ "runsFirst": option<bool>,
313
+ "contractRegister": option<Internal.onBlockContractRegisterArgs => promise<unit>>,
314
+ }
315
+ )
307
316
  HandlerRegister.registerOnBlock(
308
317
  ~name=raw["name"],
309
318
  ~where=raw["where"],
310
319
  ~fields=raw["fields"],
311
320
  ~handler=handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>),
321
+ // Not part of the public API: the subgraph runtime's `once` handlers, and
322
+ // the register passes of block handlers that create data sources.
323
+ ~runsFirst=Env.subgraphMode.contents && raw["runsFirst"] === Some(true),
324
+ ~contractRegister=?Env.subgraphMode.contents ? raw["contractRegister"] : None,
312
325
  ~getChainsObject=config => {
313
326
  let (chains, _) = buildChainsObject(~config)
314
327
  chains->(Utils.magic: {..} => dict<unknown>)
package/src/Main.res.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Env from "./Env.res.mjs";
3
4
  import * as Tui from "./tui/Tui.res.mjs";
4
5
  import * as Utils from "./Utils.res.mjs";
5
6
  import * as Config from "./Config.res.mjs";
@@ -255,7 +256,7 @@ function getGlobalIndexer() {
255
256
  };
256
257
  let onBlockFn = (rawOptions, handler) => {
257
258
  HandlerRegister.throwIfFinishedRegistration("onBlock");
258
- HandlerRegister.registerOnBlock(rawOptions.name, rawOptions.where, rawOptions.fields, handler, config => buildChainsObject(config)[0]);
259
+ HandlerRegister.registerOnBlock(rawOptions.name, rawOptions.where, rawOptions.fields, handler, Env.subgraphMode.contents && rawOptions.runsFirst === true, Env.subgraphMode.contents ? rawOptions.contractRegister : undefined, config => buildChainsObject(config)[0]);
259
260
  };
260
261
  let keysMemo = {
261
262
  contents: undefined
@@ -413,7 +414,7 @@ async function startIndexer(config, logger, persistence, resetOpt, isTestOpt, ex
413
414
  Metrics.startRuntimeCollectors();
414
415
  Server.startServer(logger, getMetrics, envioVersion, () => Persistence.getInitializedStorageOrThrow(persistence$1).dumpEffectCache().then(prim => {}), Metrics.collectRuntime, isDevelopmentMode);
415
416
  }
416
- let state = IndexerState.makeFromDbState(config$2, logger, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, Stdlib_Option.mapOr(Worker.config, false, worker => worker.holdRealtime), onError, undefined);
417
+ let state = IndexerState.makeFromDbState(config$2, logger, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, Stdlib_Option.mapOr(Worker.config, false, worker => worker.holdRealtime), onError, undefined, undefined);
417
418
  if (shouldUseTui) {
418
419
  Tui.start(config$2, logger, () => IndexerState.toMetrics(state));
419
420
  }
@@ -454,4 +455,4 @@ export {
454
455
  FatalError,
455
456
  start,
456
457
  }
457
- /* Tui Not a pure module */
458
+ /* Env Not a pure module */