envio 3.3.0-alpha.11 → 3.3.0-alpha.13

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 (40) hide show
  1. package/index.d.ts +19 -0
  2. package/package.json +6 -6
  3. package/src/ChainFetching.res +21 -12
  4. package/src/ChainFetching.res.mjs +20 -9
  5. package/src/ChainState.res +20 -8
  6. package/src/ChainState.res.mjs +13 -12
  7. package/src/ChainState.resi +1 -1
  8. package/src/CrossChainState.res +64 -41
  9. package/src/CrossChainState.res.mjs +18 -13
  10. package/src/EffectState.res +100 -0
  11. package/src/EffectState.res.mjs +74 -0
  12. package/src/EffectState.resi +32 -0
  13. package/src/Envio.res +25 -7
  14. package/src/Envio.res.mjs +15 -19
  15. package/src/FetchState.res +115 -76
  16. package/src/FetchState.res.mjs +114 -104
  17. package/src/InMemoryStore.res +14 -26
  18. package/src/InMemoryStore.res.mjs +6 -19
  19. package/src/IndexerState.res +10 -19
  20. package/src/IndexerState.res.mjs +7 -6
  21. package/src/IndexerState.resi +1 -9
  22. package/src/Internal.res +87 -12
  23. package/src/Internal.res.mjs +96 -2
  24. package/src/LoadLayer.res +44 -24
  25. package/src/LoadLayer.res.mjs +43 -20
  26. package/src/LoadLayer.resi +1 -0
  27. package/src/Persistence.res +12 -3
  28. package/src/PgStorage.res +155 -82
  29. package/src/PgStorage.res.mjs +130 -77
  30. package/src/Prometheus.res +20 -10
  31. package/src/Prometheus.res.mjs +22 -10
  32. package/src/Rollback.res +32 -13
  33. package/src/Rollback.res.mjs +24 -11
  34. package/src/UserContext.res +62 -23
  35. package/src/UserContext.res.mjs +26 -6
  36. package/src/Writing.res +28 -12
  37. package/src/Writing.res.mjs +15 -7
  38. package/src/bindings/NodeJs.res +5 -0
  39. package/src/sources/SvmHyperSyncSource.res +10 -0
  40. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
@@ -45,26 +45,13 @@ module EntityTables = {
45
45
  }
46
46
  }
47
47
 
48
- type effectCacheInMemTable = {
49
- // Cache keys whose handler output is persisted on the next write. Drained
50
- // each write; eviction is driven by the per-entry checkpointId instead.
51
- mutable idsToStore: array<string>,
52
- mutable invalidationsCount: int,
53
- // Each entry is stamped with the checkpoint that referenced it (or
54
- // loadedFromDbCheckpointId for db reads), so committed entries can be
55
- // dropped once persisted/re-derivable, mirroring entity changes.
56
- mutable dict: dict<Change.t<Internal.effectOutput>>,
57
- mutable changesCount: float,
58
- effect: Internal.effect,
59
- }
60
-
61
48
  type t = {
62
49
  config: Config.t,
63
50
  persistence: Persistence.t,
64
51
  // --- In-memory store: entity/effect tables and the pending-write queue. ---
65
52
  allEntities: array<Internal.entityConfig>,
66
53
  mutable entities: EntityTables.t,
67
- mutable effects: dict<effectCacheInMemTable>,
54
+ effectState: EffectState.t,
68
55
  mutable rollback: option<Persistence.rollback>,
69
56
  // Last checkpoint persisted to the db.
70
57
  mutable committedCheckpointId: Internal.checkpointId,
@@ -143,7 +130,7 @@ let make = (
143
130
  persistence,
144
131
  allEntities: persistence.allEntities,
145
132
  entities: EntityTables.make(persistence.allEntities),
146
- effects: Dict.make(),
133
+ effectState: EffectState.make(),
147
134
  rollback: None,
148
135
  committedCheckpointId,
149
136
  processedCheckpointId: committedCheckpointId,
@@ -211,8 +198,12 @@ let makeFromDbState = (
211
198
 
212
199
  Prometheus.ProcessingMaxBatchSize.set(~maxBatchSize=config.batchSize)
213
200
  Prometheus.ReorgThreshold.set(~isInReorgThreshold)
214
- initialState.cache->Utils.Dict.forEach(({effectName, count}) => {
215
- Prometheus.EffectCacheCount.set(~count, ~effectName)
201
+ initialState.cache->Utils.Dict.forEach(({effectName, count, scope}) => {
202
+ Prometheus.EffectCacheCount.set(
203
+ ~count,
204
+ ~effectName,
205
+ ~scope=scope->Internal.EffectCache.scopeToString,
206
+ )
216
207
  })
217
208
 
218
209
  // updateSyncTimeOnRestart wipes the saved timestamp so a restart re-enters
@@ -385,7 +376,7 @@ let config = (state: t) => state.config
385
376
  let persistence = (state: t) => state.persistence
386
377
  let allEntities = (state: t) => state.allEntities
387
378
  let entities = (state: t) => state.entities
388
- let effects = (state: t) => state.effects
379
+ let effectState = (state: t) => state.effectState
389
380
  let committedCheckpointId = (state: t) => state.committedCheckpointId
390
381
  let processedCheckpointId = (state: t) => state.processedCheckpointId
391
382
  let processedBatches = (state: t) => state.processedBatches
@@ -485,7 +476,7 @@ let beginRollbackDiff = (
485
476
  ~progressBlockNumberByChainId,
486
477
  ) => {
487
478
  state.entities = EntityTables.make(state.allEntities)
488
- state.effects = Dict.make()
479
+ state.effectState->EffectState.resetForRollback
489
480
  state.rollback = Some({
490
481
  targetCheckpointId,
491
482
  diffCheckpointId,
@@ -9,6 +9,7 @@ import * as Internal from "./Internal.res.mjs";
9
9
  import * as Throttler from "./Throttler.res.mjs";
10
10
  import * as ChainState from "./ChainState.res.mjs";
11
11
  import * as Prometheus from "./Prometheus.res.mjs";
12
+ import * as EffectState from "./EffectState.res.mjs";
12
13
  import * as LoadManager from "./LoadManager.res.mjs";
13
14
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
14
15
  import * as InMemoryTable from "./InMemoryTable.res.mjs";
@@ -70,7 +71,7 @@ function make$2(config, persistence, chainStates, isInReorgThreshold, isRealtime
70
71
  persistence: persistence,
71
72
  allEntities: persistence.allEntities,
72
73
  entities: make$1(persistence.allEntities),
73
- effects: {},
74
+ effectState: EffectState.make(),
74
75
  rollback: undefined,
75
76
  committedCheckpointId: committedCheckpointId,
76
77
  processedCheckpointId: committedCheckpointId,
@@ -116,7 +117,7 @@ function makeFromDbState(config, persistence, initialState, registrationsByChain
116
117
  Prometheus.ReorgThreshold.set(isInReorgThreshold);
117
118
  Utils.Dict.forEach(initialState.cache, param => {
118
119
  let count = param.count;
119
- Prometheus.EffectCacheCount.set(count, param.effectName);
120
+ Prometheus.EffectCacheCount.set(count, param.effectName, Internal.EffectCache.scopeToString(param.scope));
120
121
  });
121
122
  let isRealtime = !Env.updateSyncTimeOnRestart && initialState.chains.length !== 0 && initialState.chains.every(c => Stdlib_Option.isSome(c.timestampCaughtUpToHeadOrEndblock));
122
123
  let chainStates = {};
@@ -266,8 +267,8 @@ function entities(state) {
266
267
  return state.entities;
267
268
  }
268
269
 
269
- function effects(state) {
270
- return state.effects;
270
+ function effectState(state) {
271
+ return state.effectState;
271
272
  }
272
273
 
273
274
  function committedCheckpointId(state) {
@@ -419,7 +420,7 @@ function markCommitted(state, upToCheckpointId) {
419
420
 
420
421
  function beginRollbackDiff(state, targetCheckpointId, diffCheckpointId, progressBlockNumberByChainId) {
421
422
  state.entities = make$1(state.allEntities);
422
- state.effects = {};
423
+ EffectState.resetForRollback(state.effectState);
423
424
  state.rollback = {
424
425
  targetCheckpointId: targetCheckpointId,
425
426
  diffCheckpointId: diffCheckpointId,
@@ -512,7 +513,7 @@ export {
512
513
  persistence,
513
514
  allEntities,
514
515
  entities,
515
- effects,
516
+ effectState,
516
517
  committedCheckpointId,
517
518
  processedCheckpointId,
518
519
  processedBatches,
@@ -16,14 +16,6 @@ module EntityTables: {
16
16
  let get: (t, ~entityName: string) => InMemoryTable.Entity.t
17
17
  }
18
18
 
19
- type effectCacheInMemTable = {
20
- mutable idsToStore: array<string>,
21
- mutable invalidationsCount: int,
22
- mutable dict: dict<Change.t<Internal.effectOutput>>,
23
- mutable changesCount: float,
24
- effect: Internal.effect,
25
- }
26
-
27
19
  type t
28
20
 
29
21
  let make: (
@@ -86,7 +78,7 @@ let config: t => Config.t
86
78
  let persistence: t => Persistence.t
87
79
  let allEntities: t => array<Internal.entityConfig>
88
80
  let entities: t => EntityTables.t
89
- let effects: t => dict<effectCacheInMemTable>
81
+ let effectState: t => EffectState.t
90
82
  let committedCheckpointId: t => Internal.checkpointId
91
83
  let processedCheckpointId: t => Internal.checkpointId
92
84
  let processedBatches: t => array<Batch.t>
package/src/Internal.res CHANGED
@@ -752,33 +752,108 @@ type effectCacheItem = {id: string, output: effectOutput}
752
752
  type effectCacheStorageMeta = {
753
753
  itemSchema: S.t<effectCacheItem>,
754
754
  outputSchema: S.t<effectOutput>,
755
- table: Table.table,
756
755
  }
757
- type rateLimitState = {
756
+ type rateLimitOptions = {
758
757
  callsPerDuration: int,
759
758
  durationMs: int,
760
- mutable availableCalls: int,
761
- mutable windowStartTime: float,
762
- mutable queueCount: int,
763
- mutable nextWindowPromise: option<promise<unit>>,
764
759
  }
765
760
  type effect = {
766
761
  name: string,
767
762
  handler: effectArgs => promise<effectOutput>,
768
763
  storageMeta: effectCacheStorageMeta,
769
764
  defaultShouldCache: bool,
765
+ // When true (the default) a single cache is shared across every chain and the
766
+ // handler must not read context.chain. When false the cache is isolated per
767
+ // chain and context.chain.id is available.
768
+ crossChain: bool,
770
769
  output: S.t<effectOutput>,
771
770
  input: S.t<effectInput>,
772
- // The number of functions that are currently running.
773
- mutable activeCallsCount: int,
774
- mutable prevCallStartTimerRef: Performance.timeRef,
775
- rateLimit: option<rateLimitState>,
771
+ rateLimit: option<rateLimitOptions>,
776
772
  }
773
+
774
+ // Whether some piece of data (currently an effect cache; entities in a future
775
+ // version) is shared across every chain or isolated to a single chain. Unboxed:
776
+ // `CrossChain` is the string "crossChain" and `Chain(id)` is the raw chain id,
777
+ // discriminated by runtime type.
778
+ @unboxed
779
+ type chainScope =
780
+ | @as("crossChain") CrossChain
781
+ | Chain(int)
782
+
777
783
  let cacheTablePrefix = "envio_effect_"
784
+
785
+ // The single reversible mapping between an effect's (name, scope) and its
786
+ // canonical Postgres cache-table name and .envio/cache file path. Everything
787
+ // that needs a cache address goes through here instead of slicing prefixes.
788
+ // CrossChain -> envio_effect_<name> <name>.tsv
789
+ // Chain(1) -> envio_1_effect_<name> 1/<name>.tsv
790
+ // Chain(137) -> envio_137_effect_<name> 137/<name>.tsv
791
+ module EffectCache = {
792
+ let toTableName = (~effectName, ~scope) =>
793
+ switch scope {
794
+ | CrossChain => cacheTablePrefix ++ effectName
795
+ | Chain(chainId) => `envio_${chainId->Int.toString}_effect_${effectName}`
796
+ }
797
+
798
+ // "crossChain" or the decimal chain id. Used as the `scope` Prometheus label.
799
+ let scopeToString = scope =>
800
+ switch scope {
801
+ | CrossChain => "crossChain"
802
+ | Chain(chainId) => chainId->Int.toString
803
+ }
804
+
805
+ // Only accepts a canonical decimal chain id ("7", not "007" or "1foo") —
806
+ // Int.fromString alone follows parseInt semantics and accepts both.
807
+ let parseChainId = str =>
808
+ switch Int.fromString(str) {
809
+ | Some(chainId) if chainId >= 0 && chainId->Int.toString === str => Some(chainId)
810
+ | _ => None
811
+ }
812
+
813
+ let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/
814
+ let crossChainRe = /^envio_effect_(.+)$/
815
+
816
+ // Inverse of toTableName. Returns None for any table name that isn't a cache
817
+ // table. Chain-scoped is tried first: the `_effect_` separator keeps effect
818
+ // names that themselves start with digits unambiguous.
819
+ let fromTableName = (tableName): option<(string, chainScope)> =>
820
+ switch RegExp.exec(chainScopedRe, tableName) {
821
+ | Some(result) =>
822
+ switch (
823
+ RegExp.Result.matches(result)->Array.get(0),
824
+ RegExp.Result.matches(result)->Array.get(1),
825
+ ) {
826
+ | (Some(Some(chainIdStr)), Some(Some(effectName))) =>
827
+ switch parseChainId(chainIdStr) {
828
+ | Some(chainId) => Some((effectName, Chain(chainId)))
829
+ | None => None
830
+ }
831
+ | _ => None
832
+ }
833
+ | None =>
834
+ switch RegExp.exec(crossChainRe, tableName) {
835
+ | Some(result) =>
836
+ switch RegExp.Result.matches(result)->Array.get(0) {
837
+ | Some(Some(effectName)) => Some((effectName, CrossChain))
838
+ | _ => None
839
+ }
840
+ | None => None
841
+ }
842
+ }
843
+
844
+ // Relative posix path within .envio/cache. Chain-scoped caches live one
845
+ // directory level deep, named by chain id.
846
+ let toCachePath = (~effectName, ~scope) =>
847
+ switch scope {
848
+ | CrossChain => effectName ++ ".tsv"
849
+ | Chain(chainId) => `${chainId->Int.toString}/${effectName}.tsv`
850
+ }
851
+ }
852
+
778
853
  let cacheOutputSchema = S.json(~validate=false)->(Utils.magic: S.t<JSON.t> => S.t<effectOutput>)
779
- let makeCacheTable = (~effectName) => {
854
+ let makeCacheTable = (~effectName, ~scope) => {
780
855
  Table.mkTable(
781
- cacheTablePrefix ++ effectName,
856
+ EffectCache.toTableName(~effectName, ~scope),
782
857
  ~fields=[
783
858
  Table.mkField("id", String, ~fieldSchema=S.string, ~isPrimaryKey=true),
784
859
  Table.mkField("output", Json, ~fieldSchema=cacheOutputSchema, ~isNullable=true),
@@ -3,6 +3,8 @@
3
3
  import * as Table from "./db/Table.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
5
  import * as Address from "./Address.res.mjs";
6
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
7
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
8
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
7
9
 
8
10
  let allEvmBlockFields = [
@@ -166,10 +168,101 @@ let fuelTransferParamsSchema = S$RescriptSchema.schema(s => ({
166
168
 
167
169
  let cacheTablePrefix = "envio_effect_";
168
170
 
171
+ function toTableName(effectName, scope) {
172
+ if (scope === "crossChain") {
173
+ return cacheTablePrefix + effectName;
174
+ } else {
175
+ return `envio_` + scope.toString() + `_effect_` + effectName;
176
+ }
177
+ }
178
+
179
+ function scopeToString(scope) {
180
+ if (scope === "crossChain") {
181
+ return "crossChain";
182
+ } else {
183
+ return scope.toString();
184
+ }
185
+ }
186
+
187
+ function parseChainId(str) {
188
+ let chainId = Stdlib_Int.fromString(str, undefined);
189
+ if (chainId !== undefined && chainId >= 0 && chainId.toString() === str) {
190
+ return chainId;
191
+ }
192
+ }
193
+
194
+ let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/;
195
+
196
+ let crossChainRe = /^envio_effect_(.+)$/;
197
+
198
+ function fromTableName(tableName) {
199
+ let result = chainScopedRe.exec(tableName);
200
+ if (result == null) {
201
+ let result$1 = crossChainRe.exec(tableName);
202
+ if (result$1 == null) {
203
+ return;
204
+ }
205
+ let match = result$1.slice(1)[0];
206
+ if (match === undefined) {
207
+ return;
208
+ }
209
+ let effectName = Primitive_option.valFromOption(match);
210
+ if (effectName !== undefined) {
211
+ return [
212
+ effectName,
213
+ "crossChain"
214
+ ];
215
+ } else {
216
+ return;
217
+ }
218
+ }
219
+ let match$1 = result.slice(1)[0];
220
+ let match$2 = result.slice(1)[1];
221
+ if (match$1 === undefined) {
222
+ return;
223
+ }
224
+ let chainIdStr = Primitive_option.valFromOption(match$1);
225
+ if (chainIdStr === undefined) {
226
+ return;
227
+ }
228
+ if (match$2 === undefined) {
229
+ return;
230
+ }
231
+ let effectName$1 = Primitive_option.valFromOption(match$2);
232
+ if (effectName$1 === undefined) {
233
+ return;
234
+ }
235
+ let chainId = parseChainId(chainIdStr);
236
+ if (chainId !== undefined) {
237
+ return [
238
+ effectName$1,
239
+ chainId
240
+ ];
241
+ }
242
+ }
243
+
244
+ function toCachePath(effectName, scope) {
245
+ if (scope === "crossChain") {
246
+ return effectName + ".tsv";
247
+ } else {
248
+ return scope.toString() + `/` + effectName + `.tsv`;
249
+ }
250
+ }
251
+
252
+ let EffectCache = {
253
+ toTableName: toTableName,
254
+ scopeToString: scopeToString,
255
+ parseChainId: parseChainId,
256
+ chainScopedRe: chainScopedRe,
257
+ crossChainRe: crossChainRe,
258
+ fromTableName: fromTableName,
259
+ toCachePath: toCachePath
260
+ };
261
+
169
262
  let cacheOutputSchema = S$RescriptSchema.json(false);
170
263
 
171
- function makeCacheTable(effectName) {
172
- return Table.mkTable(cacheTablePrefix + effectName, undefined, [
264
+ function makeCacheTable(effectName, scope) {
265
+ return Table.mkTable(toTableName(effectName, scope), undefined, [
173
266
  Table.mkField("id", "String", S$RescriptSchema.string, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined),
174
267
  Table.mkField("output", "Json", cacheOutputSchema, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined)
175
268
  ], undefined);
@@ -195,6 +288,7 @@ export {
195
288
  fuelSupplyParamsSchema,
196
289
  fuelTransferParamsSchema,
197
290
  cacheTablePrefix,
291
+ EffectCache,
198
292
  cacheOutputSchema,
199
293
  makeCacheTable,
200
294
  loadedFromDbCheckpointId,
package/src/LoadLayer.res CHANGED
@@ -72,28 +72,29 @@ let loadById = (
72
72
  let callEffect = (
73
73
  ~effect: Internal.effect,
74
74
  ~arg: Internal.effectArgs,
75
- ~inMemTable: IndexerState.effectCacheInMemTable,
75
+ ~inMemTable: EffectState.effectCacheInMemTable,
76
76
  ~timerRef,
77
77
  ~onError,
78
78
  ) => {
79
79
  let effectName = effect.name
80
- let hadActiveCalls = effect.activeCallsCount > 0
81
- effect.activeCallsCount = effect.activeCallsCount + 1
80
+ let scopeLabel = inMemTable.scope->Internal.EffectCache.scopeToString
81
+ let hadActiveCalls = inMemTable.activeCallsCount > 0
82
+ inMemTable.activeCallsCount = inMemTable.activeCallsCount + 1
82
83
  Prometheus.EffectCalls.activeCallsCount->Prometheus.SafeGauge.handleInt(
83
- ~labels=effectName,
84
- ~value=effect.activeCallsCount,
84
+ ~labels={"effect": effectName, "scope": scopeLabel},
85
+ ~value=inMemTable.activeCallsCount,
85
86
  )
86
87
 
87
88
  if hadActiveCalls {
88
- let elapsed = Performance.secondsBetween(~from=effect.prevCallStartTimerRef, ~to=timerRef)
89
+ let elapsed = Performance.secondsBetween(~from=inMemTable.prevCallStartTimerRef, ~to=timerRef)
89
90
  if elapsed > 0. {
90
91
  Prometheus.EffectCalls.timeCounter->Prometheus.SafeCounter.handleFloat(
91
- ~labels=effectName,
92
+ ~labels={"effect": effectName, "scope": scopeLabel},
92
93
  ~value=elapsed,
93
94
  )
94
95
  }
95
96
  }
96
- effect.prevCallStartTimerRef = timerRef
97
+ inMemTable.prevCallStartTimerRef = timerRef
97
98
 
98
99
  effect.handler(arg)
99
100
  ->Promise.thenResolve(output => {
@@ -108,21 +109,23 @@ let callEffect = (
108
109
  onError(~inputKey=arg.cacheKey, ~exn)
109
110
  })
110
111
  ->Promise.finally(() => {
111
- effect.activeCallsCount = effect.activeCallsCount - 1
112
+ inMemTable.activeCallsCount = inMemTable.activeCallsCount - 1
112
113
  Prometheus.EffectCalls.activeCallsCount->Prometheus.SafeGauge.handleInt(
113
- ~labels=effectName,
114
- ~value=effect.activeCallsCount,
114
+ ~labels={"effect": effectName, "scope": scopeLabel},
115
+ ~value=inMemTable.activeCallsCount,
115
116
  )
116
117
  let newTimer = Performance.now()
117
118
  Prometheus.EffectCalls.timeCounter->Prometheus.SafeCounter.handleFloat(
118
- ~labels=effectName,
119
- ~value=Performance.secondsBetween(~from=effect.prevCallStartTimerRef, ~to=newTimer),
119
+ ~labels={"effect": effectName, "scope": scopeLabel},
120
+ ~value=Performance.secondsBetween(~from=inMemTable.prevCallStartTimerRef, ~to=newTimer),
120
121
  )
121
- effect.prevCallStartTimerRef = newTimer
122
+ inMemTable.prevCallStartTimerRef = newTimer
122
123
 
123
- Prometheus.EffectCalls.totalCallsCount->Prometheus.SafeCounter.increment(~labels=effectName)
124
+ Prometheus.EffectCalls.totalCallsCount->Prometheus.SafeCounter.increment(
125
+ ~labels={"effect": effectName, "scope": scopeLabel},
126
+ )
124
127
  Prometheus.EffectCalls.sumTimeCounter->Prometheus.SafeCounter.handleFloat(
125
- ~labels=effectName,
128
+ ~labels={"effect": effectName, "scope": scopeLabel},
126
129
  ~value=timerRef->Performance.secondsSince,
127
130
  )
128
131
  })
@@ -131,7 +134,7 @@ let callEffect = (
131
134
  let rec executeWithRateLimit = (
132
135
  ~effect: Internal.effect,
133
136
  ~effectArgs: array<Internal.effectArgs>,
134
- ~inMemTable,
137
+ ~inMemTable: EffectState.effectCacheInMemTable,
135
138
  ~onError,
136
139
  ~isFromQueue: bool,
137
140
  ) => {
@@ -140,7 +143,7 @@ let rec executeWithRateLimit = (
140
143
  let timerRef = Performance.now()
141
144
  let promises = []
142
145
 
143
- switch effect.rateLimit {
146
+ switch inMemTable.rateLimitState {
144
147
  | None =>
145
148
  // No rate limiting - execute all immediately
146
149
  for idx in 0 to effectArgs->Array.length - 1 {
@@ -193,7 +196,11 @@ let rec executeWithRateLimit = (
193
196
  if immediateCount > 0 && isFromQueue {
194
197
  // Update queue count metric
195
198
  state.queueCount = state.queueCount - immediateCount
196
- Prometheus.EffectQueueCount.set(~count=state.queueCount, ~effectName)
199
+ Prometheus.EffectQueueCount.set(
200
+ ~count=state.queueCount,
201
+ ~effectName,
202
+ ~scope=inMemTable.scope->Internal.EffectCache.scopeToString,
203
+ )
197
204
  }
198
205
 
199
206
  // Handle queued items
@@ -201,7 +208,11 @@ let rec executeWithRateLimit = (
201
208
  if !isFromQueue {
202
209
  // Update queue count metric
203
210
  state.queueCount = state.queueCount + queuedArgs->Array.length
204
- Prometheus.EffectQueueCount.set(~count=state.queueCount, ~effectName)
211
+ Prometheus.EffectQueueCount.set(
212
+ ~count=state.queueCount,
213
+ ~effectName,
214
+ ~scope=inMemTable.scope->Internal.EffectCache.scopeToString,
215
+ )
205
216
  }
206
217
 
207
218
  let millisUntilReset = ref(0)
@@ -249,14 +260,23 @@ let loadEffect = (
249
260
  ~persistence: Persistence.t,
250
261
  ~effect: Internal.effect,
251
262
  ~effectArgs,
263
+ ~scope: Internal.chainScope,
252
264
  ~indexerState,
253
265
  ~shouldGroup,
254
266
  ~item,
255
267
  ~ecosystem,
256
268
  ) => {
257
269
  let effectName = effect.name
258
- let key = `${effectName}.effect`
259
- let inMemTable = indexerState->InMemoryStore.getEffectInMemTable(~effect)
270
+ let inMemTable = indexerState->InMemoryStore.getEffectInMemTable(~effect, ~scope)
271
+ let table = inMemTable.table
272
+ let tableName = table.tableName
273
+ // The operation key must differ per scope so chain-scoped calls with the same
274
+ // input never dedup across chains. Cross-chain keeps the bare effect name so
275
+ // the storage-load metric stays stable.
276
+ let key = switch scope {
277
+ | CrossChain => `${effectName}.effect`
278
+ | Chain(chainId) => `${effectName}.effect.${chainId->Int.toString}`
279
+ }
260
280
 
261
281
  let load = async (args, ~onError) => {
262
282
  let idsToLoad = args->Array.map((arg: Internal.effectArgs) => arg.cacheKey)
@@ -264,13 +284,13 @@ let loadEffect = (
264
284
 
265
285
  if (
266
286
  switch persistence.storageStatus {
267
- | Ready({cache}) => cache->Dict.has(effectName)
287
+ | Ready({cache}) => cache->Dict.has(tableName)
268
288
  | _ => false
269
289
  }
270
290
  ) {
271
291
  let storage = persistence->Persistence.getInitializedStorageOrThrow
272
292
  let timerRef = Prometheus.StorageLoad.startOperation(~storage=storage.name, ~operation=key)
273
- let {table, outputSchema} = effect.storageMeta
293
+ let {outputSchema} = effect.storageMeta
274
294
 
275
295
  let dbEntities = try {
276
296
  (
@@ -3,6 +3,7 @@
3
3
  import * as Table from "./db/Table.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
5
  import * as Logging from "./Logging.res.mjs";
6
+ import * as Internal from "./Internal.res.mjs";
6
7
  import * as Ecosystem from "./Ecosystem.res.mjs";
7
8
  import * as Prometheus from "./Prometheus.res.mjs";
8
9
  import * as LoadManager from "./LoadManager.res.mjs";
@@ -51,24 +52,43 @@ function loadById(loadManager, persistence, entityConfig, indexerState, shouldGr
51
52
 
52
53
  function callEffect(effect, arg, inMemTable, timerRef, onError) {
53
54
  let effectName = effect.name;
54
- let hadActiveCalls = effect.activeCallsCount > 0;
55
- effect.activeCallsCount = effect.activeCallsCount + 1 | 0;
56
- Prometheus.SafeGauge.handleInt(Prometheus.EffectCalls.activeCallsCount, effectName, effect.activeCallsCount);
55
+ let scopeLabel = Internal.EffectCache.scopeToString(inMemTable.scope);
56
+ let hadActiveCalls = inMemTable.activeCallsCount > 0;
57
+ inMemTable.activeCallsCount = inMemTable.activeCallsCount + 1 | 0;
58
+ Prometheus.SafeGauge.handleInt(Prometheus.EffectCalls.activeCallsCount, {
59
+ effect: effectName,
60
+ scope: scopeLabel
61
+ }, inMemTable.activeCallsCount);
57
62
  if (hadActiveCalls) {
58
- let elapsed = Performance.secondsBetween(effect.prevCallStartTimerRef, timerRef);
63
+ let elapsed = Performance.secondsBetween(inMemTable.prevCallStartTimerRef, timerRef);
59
64
  if (elapsed > 0) {
60
- Prometheus.SafeCounter.handleFloat(Prometheus.EffectCalls.timeCounter, effectName, elapsed);
65
+ Prometheus.SafeCounter.handleFloat(Prometheus.EffectCalls.timeCounter, {
66
+ effect: effectName,
67
+ scope: scopeLabel
68
+ }, elapsed);
61
69
  }
62
70
  }
63
- effect.prevCallStartTimerRef = timerRef;
71
+ inMemTable.prevCallStartTimerRef = timerRef;
64
72
  return effect.handler(arg).then(output => InMemoryStore.setEffectOutput(inMemTable, arg.checkpointId, arg.cacheKey, output, arg.context.cache)).catch(exn => onError(arg.cacheKey, exn)).finally(() => {
65
- effect.activeCallsCount = effect.activeCallsCount - 1 | 0;
66
- Prometheus.SafeGauge.handleInt(Prometheus.EffectCalls.activeCallsCount, effectName, effect.activeCallsCount);
73
+ inMemTable.activeCallsCount = inMemTable.activeCallsCount - 1 | 0;
74
+ Prometheus.SafeGauge.handleInt(Prometheus.EffectCalls.activeCallsCount, {
75
+ effect: effectName,
76
+ scope: scopeLabel
77
+ }, inMemTable.activeCallsCount);
67
78
  let newTimer = Performance.now();
68
- Prometheus.SafeCounter.handleFloat(Prometheus.EffectCalls.timeCounter, effectName, Performance.secondsBetween(effect.prevCallStartTimerRef, newTimer));
69
- effect.prevCallStartTimerRef = newTimer;
70
- Prometheus.SafeCounter.increment(Prometheus.EffectCalls.totalCallsCount, effectName);
71
- Prometheus.SafeCounter.handleFloat(Prometheus.EffectCalls.sumTimeCounter, effectName, Performance.secondsSince(timerRef));
79
+ Prometheus.SafeCounter.handleFloat(Prometheus.EffectCalls.timeCounter, {
80
+ effect: effectName,
81
+ scope: scopeLabel
82
+ }, Performance.secondsBetween(inMemTable.prevCallStartTimerRef, newTimer));
83
+ inMemTable.prevCallStartTimerRef = newTimer;
84
+ Prometheus.SafeCounter.increment(Prometheus.EffectCalls.totalCallsCount, {
85
+ effect: effectName,
86
+ scope: scopeLabel
87
+ });
88
+ Prometheus.SafeCounter.handleFloat(Prometheus.EffectCalls.sumTimeCounter, {
89
+ effect: effectName,
90
+ scope: scopeLabel
91
+ }, Performance.secondsSince(timerRef));
72
92
  });
73
93
  }
74
94
 
@@ -76,7 +96,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
76
96
  let effectName = effect.name;
77
97
  let timerRef = Performance.now();
78
98
  let promises = [];
79
- let state = effect.rateLimit;
99
+ let state = inMemTable.rateLimitState;
80
100
  if (state !== undefined) {
81
101
  let now = Date.now();
82
102
  if (now >= state.windowStartTime + state.durationMs) {
@@ -93,12 +113,12 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
93
113
  }
94
114
  if (immediateCount > 0 && isFromQueue) {
95
115
  state.queueCount = state.queueCount - immediateCount | 0;
96
- Prometheus.EffectQueueCount.set(state.queueCount, effectName);
116
+ Prometheus.EffectQueueCount.set(state.queueCount, effectName, Internal.EffectCache.scopeToString(inMemTable.scope));
97
117
  }
98
118
  if (Utils.$$Array.notEmpty(queuedArgs)) {
99
119
  if (!isFromQueue) {
100
120
  state.queueCount = state.queueCount + queuedArgs.length | 0;
101
- Prometheus.EffectQueueCount.set(state.queueCount, effectName);
121
+ Prometheus.EffectQueueCount.set(state.queueCount, effectName, Internal.EffectCache.scopeToString(inMemTable.scope));
102
122
  }
103
123
  let millisUntilReset = {
104
124
  contents: 0
@@ -128,16 +148,19 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
128
148
  return Promise.all(promises);
129
149
  }
130
150
 
131
- function loadEffect(loadManager, persistence, effect, effectArgs, indexerState, shouldGroup, item, ecosystem) {
151
+ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexerState, shouldGroup, item, ecosystem) {
132
152
  let effectName = effect.name;
133
- let key = effectName + `.effect`;
134
- let inMemTable = InMemoryStore.getEffectInMemTable(indexerState, effect);
153
+ let inMemTable = InMemoryStore.getEffectInMemTable(indexerState, effect, scope);
154
+ let table = inMemTable.table;
155
+ let tableName = table.tableName;
156
+ let key;
157
+ key = scope === "crossChain" ? effectName + `.effect` : effectName + `.effect.` + scope.toString();
135
158
  let load = async (args, onError) => {
136
159
  let idsToLoad = args.map(arg => arg.cacheKey);
137
160
  let idsFromCache = new Set();
138
161
  let match = persistence.storageStatus;
139
162
  let tmp;
140
- tmp = typeof match !== "object" || match.TAG === "Initializing" ? false : effectName in match._0.cache;
163
+ tmp = typeof match !== "object" || match.TAG === "Initializing" ? false : tableName in match._0.cache;
141
164
  if (tmp) {
142
165
  let storage = Persistence.getInitializedStorageOrThrow(persistence);
143
166
  let timerRef = Prometheus.StorageLoad.startOperation(storage.name, key);
@@ -149,7 +172,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, indexerState,
149
172
  operator: "in",
150
173
  fieldName: Table.idFieldName,
151
174
  fieldValue: idsToLoad
152
- }, match$1.table);
175
+ }, table);
153
176
  } catch (raw_exn) {
154
177
  let exn = Primitive_exceptions.internalToException(raw_exn);
155
178
  Logging.childWarn(Ecosystem.getItemLogger(item, ecosystem), {
@@ -25,6 +25,7 @@ let loadEffect: (
25
25
  ~persistence: Persistence.t,
26
26
  ~effect: Internal.effect,
27
27
  ~effectArgs: Internal.effectArgs,
28
+ ~scope: Internal.chainScope,
28
29
  ~indexerState: IndexerState.t,
29
30
  ~shouldGroup: bool,
30
31
  ~item: Internal.item,