envio 3.3.0-alpha.9 → 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 (94) 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 +134 -55
  13. package/src/ChainState.res.mjs +78 -36
  14. package/src/ChainState.resi +14 -2
  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 +115 -34
  20. package/src/CrossChainState.res.mjs +37 -14
  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/EventConfigBuilder.res +21 -46
  27. package/src/EventConfigBuilder.res.mjs +18 -25
  28. package/src/EventProcessing.res +8 -5
  29. package/src/EventProcessing.res.mjs +2 -1
  30. package/src/FetchState.res +814 -467
  31. package/src/FetchState.res.mjs +541 -369
  32. package/src/HandlerRegister.res +3 -1
  33. package/src/HandlerRegister.res.mjs +3 -2
  34. package/src/InMemoryStore.res +14 -26
  35. package/src/InMemoryStore.res.mjs +6 -19
  36. package/src/IndexerState.res +15 -39
  37. package/src/IndexerState.res.mjs +18 -29
  38. package/src/IndexerState.resi +2 -10
  39. package/src/Internal.res +98 -14
  40. package/src/Internal.res.mjs +96 -2
  41. package/src/LoadLayer.res +44 -24
  42. package/src/LoadLayer.res.mjs +43 -20
  43. package/src/LoadLayer.resi +1 -0
  44. package/src/LogSelection.res +0 -62
  45. package/src/LogSelection.res.mjs +0 -74
  46. package/src/Metrics.res +1 -1
  47. package/src/Metrics.res.mjs +1 -1
  48. package/src/Persistence.res +12 -3
  49. package/src/PgStorage.res +155 -82
  50. package/src/PgStorage.res.mjs +130 -77
  51. package/src/Prometheus.res +20 -10
  52. package/src/Prometheus.res.mjs +22 -10
  53. package/src/PruneStaleHistory.res +146 -35
  54. package/src/PruneStaleHistory.res.mjs +114 -20
  55. package/src/RawEvent.res +2 -2
  56. package/src/Rollback.res +32 -13
  57. package/src/Rollback.res.mjs +24 -11
  58. package/src/SimulateDeadInputTracker.res +1 -1
  59. package/src/SimulateItems.res +38 -6
  60. package/src/SimulateItems.res.mjs +28 -9
  61. package/src/TestIndexer.res +2 -2
  62. package/src/TestIndexer.res.mjs +2 -2
  63. package/src/TopicFilter.res +1 -25
  64. package/src/TopicFilter.res.mjs +0 -74
  65. package/src/UserContext.res +62 -23
  66. package/src/UserContext.res.mjs +26 -6
  67. package/src/Writing.res +60 -21
  68. package/src/Writing.res.mjs +27 -9
  69. package/src/bindings/NodeJs.res +5 -0
  70. package/src/bindings/Viem.res +0 -5
  71. package/src/sources/EventRouter.res +0 -21
  72. package/src/sources/EventRouter.res.mjs +0 -12
  73. package/src/sources/EvmChain.res +2 -27
  74. package/src/sources/EvmChain.res.mjs +2 -23
  75. package/src/sources/EvmRpcClient.res +58 -23
  76. package/src/sources/EvmRpcClient.res.mjs +11 -5
  77. package/src/sources/HyperSync.res +9 -38
  78. package/src/sources/HyperSync.res.mjs +16 -28
  79. package/src/sources/HyperSync.resi +2 -2
  80. package/src/sources/HyperSyncClient.res +88 -11
  81. package/src/sources/HyperSyncClient.res.mjs +39 -6
  82. package/src/sources/HyperSyncSource.res +18 -199
  83. package/src/sources/HyperSyncSource.res.mjs +9 -128
  84. package/src/sources/Rpc.res +0 -32
  85. package/src/sources/Rpc.res.mjs +1 -46
  86. package/src/sources/RpcSource.res +129 -522
  87. package/src/sources/RpcSource.res.mjs +161 -366
  88. package/src/sources/SimulateSource.res +37 -19
  89. package/src/sources/SimulateSource.res.mjs +27 -10
  90. package/src/sources/SourceManager.res +3 -7
  91. package/src/sources/SourceManager.res.mjs +2 -7
  92. package/src/sources/SourceManager.resi +0 -2
  93. package/src/sources/SvmHyperSyncSource.res +13 -3
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
@@ -240,9 +240,12 @@ let deriveSrcAddress = (
240
240
  }
241
241
  }
242
242
 
243
- let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Config.chain): array<
244
- Internal.item,
245
- > => {
243
+ let parse = (
244
+ ~simulateItems: array<JSON.t>,
245
+ ~config: Config.t,
246
+ ~chainConfig: Config.chain,
247
+ ~onEventRegistrations: array<Internal.onEventRegistration>,
248
+ ): array<Internal.item> => {
246
249
  let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
247
250
  let chainId = chainConfig.id
248
251
  let startBlock = chainConfig.startBlock
@@ -345,6 +348,11 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
345
348
  ~chainId,
346
349
  ~eventConfig,
347
350
  )
351
+ // Append into the registration array that the chain state will own and
352
+ // put that same registration object directly on the simulated item.
353
+ let onEventRegistrationIndex = onEventRegistrations->Array.length
354
+ let onEventRegistration = {...onEventRegistration, index: onEventRegistrationIndex}
355
+ onEventRegistrations->Array.push(onEventRegistration)->ignore
348
356
 
349
357
  items
350
358
  ->Array.push(
@@ -382,7 +390,11 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
382
390
 
383
391
  // Apply simulate source config from processConfig JSON to a Config.t
384
392
  // This patches chainMap entries that have simulate items with CustomSources
385
- let patchConfig = (~config: Config.t, ~processConfig: JSON.t): Config.t => {
393
+ let patchConfig = (
394
+ ~config: Config.t,
395
+ ~processConfig: JSON.t,
396
+ ~registrationsByChainId: HandlerRegister.registrationsByChainId,
397
+ ): Config.t => {
386
398
  let processChains: option<dict<JSON.t>> =
387
399
  (processConfig->(Utils.magic: JSON.t => {..}))["chains"]->Nullable.toOption
388
400
  switch processChains {
@@ -395,11 +407,31 @@ let patchConfig = (~config: Config.t, ~processConfig: JSON.t): Config.t => {
395
407
  let simulateRaw: option<array<JSON.t>> = raw["simulate"]->Nullable.toOption
396
408
  switch simulateRaw {
397
409
  | Some(simulateItems) =>
398
- let items = parse(~simulateItems, ~config, ~chainConfig)
410
+ let chainRegistrations = switch registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(
411
+ chainIdStr,
412
+ ) {
413
+ | Some(registrations) => registrations
414
+ | None =>
415
+ let registrations: HandlerRegister.chainRegistrations = {
416
+ onEventRegistrations: [],
417
+ onBlockRegistrations: [],
418
+ }
419
+ registrationsByChainId->Dict.set(chainIdStr, registrations)
420
+ registrations
421
+ }
399
422
  let startBlock: int = raw["startBlock"]->(Utils.magic: 'a => int)
400
423
  let endBlock: int = raw["endBlock"]->(Utils.magic: 'a => int)
424
+ // Parse with the process's startBlock so items default into the range
425
+ // the source will be queried over; the source now filters by range.
426
+ let chainConfig = {...chainConfig, startBlock, endBlock}
427
+ let items = parse(
428
+ ~simulateItems,
429
+ ~config,
430
+ ~chainConfig,
431
+ ~onEventRegistrations=chainRegistrations.onEventRegistrations,
432
+ )
401
433
  let source = SimulateSource.make(~items, ~endBlock, ~chain)
402
- {...chainConfig, startBlock, endBlock, sourceConfig: Config.CustomSources([source])}
434
+ {...chainConfig, sourceConfig: Config.CustomSources([source])}
403
435
  | None => chainConfig
404
436
  }
405
437
  | None => chainConfig
@@ -167,7 +167,7 @@ function deriveSrcAddress(providedSrcAddress, eventConfig, chainConfig, config)
167
167
  }
168
168
  }
169
169
 
170
- function parse(simulateItems, config, chainConfig) {
170
+ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
171
171
  let chain = ChainMap.Chain.makeUnsafe(chainConfig.id);
172
172
  let chainId = chainConfig.id;
173
173
  let startBlock = chainConfig.startBlock;
@@ -254,9 +254,13 @@ function parse(simulateItems, config, chainConfig) {
254
254
  seenCoordinates[coordinate] = itemIndex;
255
255
  }
256
256
  let onEventRegistration = HandlerRegister.buildOnEventRegistration(config, chainId, eventConfig, undefined);
257
+ let onEventRegistrationIndex = onEventRegistrations.length;
258
+ let newrecord = {...onEventRegistration};
259
+ newrecord.index = onEventRegistrationIndex;
260
+ onEventRegistrations.push(newrecord);
257
261
  items.push({
258
262
  kind: 0,
259
- onEventRegistration: onEventRegistration,
263
+ onEventRegistration: newrecord,
260
264
  chain: chain,
261
265
  blockNumber: blockNumber,
262
266
  logIndex: logIndex,
@@ -276,7 +280,7 @@ function parse(simulateItems, config, chainConfig) {
276
280
  return items;
277
281
  }
278
282
 
279
- function patchConfig(config, processConfig) {
283
+ function patchConfig(config, processConfig, registrationsByChainId) {
280
284
  let processChains = processConfig.chains;
281
285
  if (processChains == null) {
282
286
  return config;
@@ -291,18 +295,33 @@ function patchConfig(config, processConfig) {
291
295
  if (simulateRaw == null) {
292
296
  return chainConfig;
293
297
  }
294
- let items = parse(simulateRaw, config, chainConfig);
298
+ let registrations = registrationsByChainId[chainIdStr];
299
+ let chainRegistrations;
300
+ if (registrations !== undefined) {
301
+ chainRegistrations = registrations;
302
+ } else {
303
+ let registrations_onEventRegistrations = [];
304
+ let registrations_onBlockRegistrations = [];
305
+ let registrations$1 = {
306
+ onEventRegistrations: registrations_onEventRegistrations,
307
+ onBlockRegistrations: registrations_onBlockRegistrations
308
+ };
309
+ registrationsByChainId[chainIdStr] = registrations$1;
310
+ chainRegistrations = registrations$1;
311
+ }
295
312
  let startBlock = processChainJson.startBlock;
296
313
  let endBlock = processChainJson.endBlock;
297
- let source = SimulateSource.make(items, endBlock, chain);
298
314
  let newrecord = {...chainConfig};
299
- newrecord.sourceConfig = {
315
+ newrecord.endBlock = endBlock;
316
+ newrecord.startBlock = startBlock;
317
+ let items = parse(simulateRaw, config, newrecord, chainRegistrations.onEventRegistrations);
318
+ let source = SimulateSource.make(items, endBlock, chain);
319
+ let newrecord$1 = {...newrecord};
320
+ newrecord$1.sourceConfig = {
300
321
  TAG: "CustomSources",
301
322
  _0: [source]
302
323
  };
303
- newrecord.endBlock = endBlock;
304
- newrecord.startBlock = startBlock;
305
- return newrecord;
324
+ return newrecord$1;
306
325
  });
307
326
  return {
308
327
  name: config.name,
@@ -863,8 +863,8 @@ let initTestWorker = () => {
863
863
  | Some(_) => ()
864
864
  }
865
865
 
866
- let patchConfig = (config: Config.t, _registrations) => {
867
- let config = SimulateItems.patchConfig(~config, ~processConfig)
866
+ let patchConfig = (config: Config.t, registrationsByChainId) => {
867
+ let config = SimulateItems.patchConfig(~config, ~processConfig, ~registrationsByChainId)
868
868
 
869
869
  // In auto-exit mode, set batchSize=1 to process one block checkpoint at a time
870
870
  if exitAfterFirstEventBlock {
@@ -612,8 +612,8 @@ function initTestWorker() {
612
612
  } else {
613
613
  Logging.setLogLevel("silent");
614
614
  }
615
- let patchConfig = (config, _registrations) => {
616
- let config$1 = SimulateItems.patchConfig(config, processConfig);
615
+ let patchConfig = (config, registrationsByChainId) => {
616
+ let config$1 = SimulateItems.patchConfig(config, processConfig, registrationsByChainId);
617
617
  if (exitAfterFirstEventBlock) {
618
618
  return {
619
619
  name: config$1.name,
@@ -1,27 +1,3 @@
1
- let toTwosComplement = (num: bigint, ~bytesLen: int) => {
2
- let maxValue = 1n->BigInt.shiftLeft(BigInt.fromInt(bytesLen * 8))
3
- let mask = maxValue->BigInt.sub(1n)
4
- num->BigInt.add(maxValue)->BigInt.bitwiseAnd(mask)
5
- }
6
-
7
- let fromSignedBigInt = val => {
8
- let bytesLen = 32
9
- let val = val >= 0n ? val : val->toTwosComplement(~bytesLen)
10
- val->Viem.bigintToHex(~options={size: bytesLen})
11
- }
12
-
13
1
  type hex = EvmTypes.Hex.t
14
- //bytes currently does not work with genType and we also currently generate bytes as a string type
15
- type bytesHex = string
16
- let keccak256 = Viem.keccak256
17
- let bytesToHex = Viem.bytesToHex
18
- let concat = Viem.concat
19
- let castToHexUnsafe: 'a => hex = val => val->(Utils.magic: 'a => hex)
20
- let fromBigInt: bigint => hex = val => val->Viem.bigintToHex(~options={size: 32})
21
- let fromDynamicString: string => hex = val => val->(Utils.magic: string => hex)->keccak256
22
- let fromString: string => hex = val => val->Viem.stringToHex(~options={size: 32})
2
+
23
3
  let fromAddress: Address.t => hex = addr => addr->(Utils.magic: Address.t => hex)->Viem.pad
24
- let fromDynamicBytes: bytesHex => hex = bytes => bytes->(Utils.magic: bytesHex => hex)->keccak256
25
- let fromBytes: bytesHex => hex = bytes =>
26
- bytes->(Utils.magic: bytesHex => Uint8Array.t)->Viem.bytesToHex(~options={size: 32})
27
- let fromBool: bool => hex = b => b->Viem.boolToHex(~options={size: 32})
@@ -1,86 +1,12 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Viem from "viem";
4
- import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
-
6
- function toTwosComplement(num, bytesLen) {
7
- let maxValue = (1n << BigInt((bytesLen << 3)));
8
- let mask = maxValue - 1n;
9
- return num + maxValue & mask;
10
- }
11
-
12
- function fromSignedBigInt(val) {
13
- let val$1 = val >= 0n ? val : toTwosComplement(val, 32);
14
- return Viem.numberToHex(val$1, {
15
- size: 32
16
- });
17
- }
18
-
19
- function keccak256(prim) {
20
- return Viem.keccak256(prim);
21
- }
22
-
23
- function bytesToHex(prim0, prim1) {
24
- return Viem.bytesToHex(prim0, prim1 !== undefined ? Primitive_option.valFromOption(prim1) : undefined);
25
- }
26
-
27
- function concat(prim) {
28
- return Viem.concat(prim);
29
- }
30
-
31
- function castToHexUnsafe(val) {
32
- return val;
33
- }
34
-
35
- function fromBigInt(val) {
36
- return Viem.numberToHex(val, {
37
- size: 32
38
- });
39
- }
40
-
41
- function fromDynamicString(val) {
42
- return Viem.keccak256(val);
43
- }
44
-
45
- function fromString(val) {
46
- return Viem.stringToHex(val, {
47
- size: 32
48
- });
49
- }
50
4
 
51
5
  function fromAddress(addr) {
52
6
  return Viem.pad(addr);
53
7
  }
54
8
 
55
- function fromDynamicBytes(bytes) {
56
- return Viem.keccak256(bytes);
57
- }
58
-
59
- function fromBytes(bytes) {
60
- return Viem.bytesToHex(bytes, {
61
- size: 32
62
- });
63
- }
64
-
65
- function fromBool(b) {
66
- return Viem.boolToHex(b, {
67
- size: 32
68
- });
69
- }
70
-
71
9
  export {
72
- toTwosComplement,
73
- fromSignedBigInt,
74
- keccak256,
75
- bytesToHex,
76
- concat,
77
- castToHexUnsafe,
78
- fromBigInt,
79
- fromDynamicString,
80
- fromString,
81
10
  fromAddress,
82
- fromDynamicBytes,
83
- fromBytes,
84
- fromBool,
85
11
  }
86
12
  /* viem Not a pure module */
@@ -28,10 +28,23 @@ Utils.Object.defineProperty(
28
28
  },
29
29
  )
30
30
  %%raw(`
31
- var EffectContext = function(params, defaultShouldCache, callEffect) {
31
+ // Top-level so the getter is created once, not per context. \`this\` is the
32
+ // EffectContext instance; only cross-chain contexts install it.
33
+ function throwCrossChainChainAccess() {
34
+ throw new Error('context.chain is not available on the cross-chain effect "' + this._effectName + '". Set \`crossChain: false\` in its options to scope the effect to a single chain, then read context.chain.id.');
35
+ }
36
+ var crossChainChainDescriptor = { get: throwCrossChainChainAccess, enumerable: true };
37
+ var EffectContext = function(params, chainId, effectName, defaultShouldCache, callEffect) {
32
38
  paramsByThis.set(this, params);
33
39
  this.effect = callEffect;
34
40
  this.cache = defaultShouldCache;
41
+ if (chainId === undefined) {
42
+ // Cross-chain: reading context.chain throws, via the shared getter.
43
+ Object.defineProperty(this, "_effectName", { value: effectName });
44
+ Object.defineProperty(this, "chain", crossChainChainDescriptor);
45
+ } else {
46
+ this.chain = { id: chainId };
47
+ }
35
48
  };
36
49
  EffectContext.prototype = effectContextPrototype;
37
50
  `)
@@ -39,35 +52,61 @@ EffectContext.prototype = effectContextPrototype;
39
52
  @new
40
53
  external makeEffectContext: (
41
54
  contextParams,
55
+ ~chainId: option<int>,
56
+ ~effectName: string,
42
57
  ~defaultShouldCache: bool,
43
58
  ~callEffect: (Internal.effect, Internal.effectInput) => promise<Internal.effectOutput>,
44
59
  ) => Internal.effectContext = "EffectContext"
45
60
 
46
61
  let initEffect = (params: contextParams) => {
47
- let rec callEffect = (effect: Internal.effect, input: Internal.effectInput) => {
48
- let effectContext = makeEffectContext(
49
- params,
50
- ~defaultShouldCache=effect.defaultShouldCache,
51
- ~callEffect,
52
- )
53
- let effectArgs: Internal.effectArgs = {
54
- input,
55
- context: effectContext,
56
- cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
57
- checkpointId: params.checkpointId,
62
+ let handlerChainId = params.item->Internal.getItemChainId
63
+ // A chain-scoped effect always resolves against the chain of the handler that
64
+ // triggered the call, even several effects deep, so the chain id is captured
65
+ // once from the item and reused for the whole nested-call tree.
66
+ let rec makeCaller = (~caller: option<Internal.effect>) => {
67
+ (effect: Internal.effect, input: Internal.effectInput) => {
68
+ let scope = effect.crossChain ? Internal.CrossChain : Internal.Chain(handlerChainId)
69
+
70
+ switch caller {
71
+ | Some(callerEffect) if callerEffect.crossChain && !effect.crossChain =>
72
+ // A cross-chain effect isn't tied to a single chain, so it has no chain
73
+ // to resolve a chain-scoped child against. Reject before any cache work.
74
+ JsError.throwWithMessage(
75
+ `The cross-chain effect "${callerEffect.name}" cannot call the chain-scoped effect "${effect.name}", because a cross-chain effect isn't tied to a single chain. Make "${effect.name}" cross-chain (\`crossChain: true\`), or make "${callerEffect.name}" chain-scoped (\`crossChain: false\`).`,
76
+ )
77
+ | _ => ()
78
+ }
79
+
80
+ let effectContext = makeEffectContext(
81
+ params,
82
+ ~chainId=switch scope {
83
+ | Internal.Chain(chainId) => Some(chainId)
84
+ | Internal.CrossChain => None
85
+ },
86
+ ~effectName=effect.name,
87
+ ~defaultShouldCache=effect.defaultShouldCache,
88
+ ~callEffect=makeCaller(~caller=Some(effect)),
89
+ )
90
+ let effectArgs: Internal.effectArgs = {
91
+ input,
92
+ context: effectContext,
93
+ cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
94
+ checkpointId: params.checkpointId,
95
+ }
96
+ LoadLayer.loadEffect(
97
+ ~loadManager=params.loadManager,
98
+ ~persistence=params.persistence,
99
+ ~effect,
100
+ ~effectArgs,
101
+ ~scope,
102
+ ~indexerState=params.indexerState,
103
+ ~shouldGroup=params.isPreload,
104
+ ~item=params.item,
105
+ ~ecosystem=params.config.ecosystem,
106
+ )
58
107
  }
59
- LoadLayer.loadEffect(
60
- ~loadManager=params.loadManager,
61
- ~persistence=params.persistence,
62
- ~effect,
63
- ~effectArgs,
64
- ~indexerState=params.indexerState,
65
- ~shouldGroup=params.isPreload,
66
- ~item=params.item,
67
- ~ecosystem=params.config.ecosystem,
68
- )
69
108
  }
70
- callEffect
109
+ makeCaller(~caller=None)
71
110
  }
72
111
 
73
112
  type entityContextParams = {
@@ -25,17 +25,37 @@ Object.defineProperty(effectContextPrototype, "log", {
25
25
  }
26
26
  });
27
27
 
28
- var EffectContext = function(params, defaultShouldCache, callEffect) {
28
+ // Top-level so the getter is created once, not per context. \`this\` is the
29
+ // EffectContext instance; only cross-chain contexts install it.
30
+ function throwCrossChainChainAccess() {
31
+ throw new Error('context.chain is not available on the cross-chain effect "' + this._effectName + '". Set \`crossChain: false\` in its options to scope the effect to a single chain, then read context.chain.id.');
32
+ }
33
+ var crossChainChainDescriptor = { get: throwCrossChainChainAccess, enumerable: true };
34
+ var EffectContext = function(params, chainId, effectName, defaultShouldCache, callEffect) {
29
35
  paramsByThis.set(this, params);
30
36
  this.effect = callEffect;
31
37
  this.cache = defaultShouldCache;
38
+ if (chainId === undefined) {
39
+ // Cross-chain: reading context.chain throws, via the shared getter.
40
+ Object.defineProperty(this, "_effectName", { value: effectName });
41
+ Object.defineProperty(this, "chain", crossChainChainDescriptor);
42
+ } else {
43
+ this.chain = { id: chainId };
44
+ }
32
45
  };
33
46
  EffectContext.prototype = effectContextPrototype;
34
47
  ;
35
48
 
36
49
  function initEffect(params) {
37
- let callEffect = (effect, input) => {
38
- let effectContext = new EffectContext(params, effect.defaultShouldCache, callEffect);
50
+ let handlerChainId = Internal.getItemChainId(params.item);
51
+ let makeCaller = caller => ((effect, input) => {
52
+ let scope = effect.crossChain ? "crossChain" : handlerChainId;
53
+ if (caller !== undefined && caller.crossChain && !effect.crossChain) {
54
+ Stdlib_JsError.throwWithMessage(`The cross-chain effect "` + caller.name + `" cannot call the chain-scoped effect "` + effect.name + `", because a cross-chain effect isn't tied to a single chain. Make "` + effect.name + `" cross-chain (\`crossChain: true\`), or make "` + caller.name + `" chain-scoped (\`crossChain: false\`).`);
55
+ }
56
+ let tmp;
57
+ tmp = scope === "crossChain" ? undefined : scope;
58
+ let effectContext = new EffectContext(params, tmp, effect.name, effect.defaultShouldCache, makeCaller(effect));
39
59
  let effectArgs_cacheKey = Utils.Hash.makeOrThrow(S$RescriptSchema.reverseConvertOrThrow(input, effect.input));
40
60
  let effectArgs_checkpointId = params.checkpointId;
41
61
  let effectArgs = {
@@ -44,9 +64,9 @@ function initEffect(params) {
44
64
  cacheKey: effectArgs_cacheKey,
45
65
  checkpointId: effectArgs_checkpointId
46
66
  };
47
- return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, params.indexerState, params.isPreload, params.item, params.config.ecosystem);
48
- };
49
- return callEffect;
67
+ return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, scope, params.indexerState, params.isPreload, params.item, params.config.ecosystem);
68
+ });
69
+ return makeCaller(undefined);
50
70
  }
51
71
 
52
72
  function getWhereHandler(params, filter) {
package/src/Writing.res CHANGED
@@ -16,8 +16,8 @@ let getChangesCount = (state: IndexerState.t) => {
16
16
  total := total.contents +. (state->InMemoryStore.getInMemTable(~entityConfig)).changesCount
17
17
  })
18
18
  state
19
- ->IndexerState.effects
20
- ->Utils.Dict.forEach(inMemTable => {
19
+ ->IndexerState.effectState
20
+ ->EffectState.forEach(inMemTable => {
21
21
  total := total.contents +. inMemTable.changesCount
22
22
  })
23
23
  state
@@ -38,9 +38,9 @@ let waitForCommit = (state: IndexerState.t): promise<unit> =>
38
38
  let snapshotEffects = (state: IndexerState.t, ~cache): array<Persistence.updatedEffectCache> => {
39
39
  let acc = []
40
40
  state
41
- ->IndexerState.effects
42
- ->Utils.Dict.forEach(inMemTable => {
43
- let {idsToStore, dict, effect, invalidationsCount} = inMemTable
41
+ ->IndexerState.effectState
42
+ ->EffectState.forEach(inMemTable => {
43
+ let {idsToStore, dict, effect, invalidationsCount, scope, table} = inMemTable
44
44
  switch idsToStore {
45
45
  | [] => ()
46
46
  | ids =>
@@ -51,17 +51,33 @@ let snapshotEffects = (state: IndexerState.t, ~cache): array<Persistence.updated
51
51
  }
52
52
  )
53
53
  let effectName = effect.name
54
- let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(effectName) {
54
+ let tableName = table.tableName
55
+ let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(tableName) {
55
56
  | Some(c) => c
56
57
  | None =>
57
- let c: Persistence.effectCacheRecord = {effectName, count: 0}
58
- cache->Dict.set(effectName, c)
58
+ let c: Persistence.effectCacheRecord = {effectName, scope, tableName, count: 0}
59
+ cache->Dict.set(tableName, c)
59
60
  c
60
61
  }
61
62
  let shouldInitialize = effectCacheRecord.count === 0
62
63
  effectCacheRecord.count = effectCacheRecord.count + items->Array.length - invalidationsCount
63
- Prometheus.EffectCacheCount.set(~count=effectCacheRecord.count, ~effectName)
64
- acc->Array.push(({effect, items, shouldInitialize}: Persistence.updatedEffectCache))->ignore
64
+ Prometheus.EffectCacheCount.set(
65
+ ~count=effectCacheRecord.count,
66
+ ~effectName,
67
+ ~scope=scope->Internal.EffectCache.scopeToString,
68
+ )
69
+ acc
70
+ ->Array.push(
71
+ (
72
+ {
73
+ table,
74
+ itemSchema: effect.storageMeta.itemSchema,
75
+ items,
76
+ shouldInitialize,
77
+ }: Persistence.updatedEffectCache
78
+ ),
79
+ )
80
+ ->ignore
65
81
  }
66
82
  inMemTable.idsToStore = []
67
83
  inMemTable.invalidationsCount = 0
@@ -113,17 +129,36 @@ let runOneWrite = async (state: IndexerState.t) => {
113
129
  })
114
130
  let updatedEffectsCache = snapshotEffects(state, ~cache)
115
131
 
116
- await persistence.storage.writeBatch(
117
- ~batch,
118
- ~rollback,
119
- ~isInReorgThreshold=batch.isInReorgThreshold,
120
- ~config,
121
- ~allEntities=persistence.allEntities,
122
- ~updatedEntities,
123
- ~updatedEffectsCache,
124
- ~chainMetaData,
132
+ let writtenEntityNames = Utils.Set.make()
133
+ updatedEntities->Array.forEach(({entityConfig}) =>
134
+ writtenEntityNames->Utils.Set.add(entityConfig.name)->ignore
135
+ )
136
+ let pruneTargets = PruneStaleHistory.select(
137
+ state,
138
+ ~writtenEntityNames,
139
+ ~isRollback=rollback->Option.isSome,
125
140
  )
126
141
 
142
+ // The prune runs concurrently with the batch write, but only for entities
143
+ // absent from it, so they never touch the same history table. Both must be
144
+ // awaited before the next write starts, otherwise a still-running prune
145
+ // could overlap the next batch's history writes and lose an anchor (which
146
+ // breaks a later rollback). Rollback writes touch every history table, so
147
+ // they get no concurrent prune at all.
148
+ let _ = await Promise.all2((
149
+ persistence.storage.writeBatch(
150
+ ~batch,
151
+ ~rollback,
152
+ ~isInReorgThreshold=batch.isInReorgThreshold,
153
+ ~config,
154
+ ~allEntities=persistence.allEntities,
155
+ ~updatedEntities,
156
+ ~updatedEffectsCache,
157
+ ~chainMetaData,
158
+ ),
159
+ PruneStaleHistory.runConcurrent(state, ~targets=pruneTargets),
160
+ ))
161
+
127
162
  state->IndexerState.markCommitted(~upToCheckpointId)
128
163
 
129
164
  switch rollback {
@@ -131,6 +166,10 @@ let runOneWrite = async (state: IndexerState.t) => {
131
166
  await RollbackCommit.fire(~progressBlockNumberByChainId)
132
167
  | _ => ()
133
168
  }
169
+
170
+ // Entities starved of the concurrent prune (eg written in every batch) are
171
+ // pruned here with no other pg writer running.
172
+ await PruneStaleHistory.runForced(state, ~targets=pruneTargets)
134
173
  }
135
174
  }
136
175
 
@@ -192,8 +231,8 @@ let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
192
231
  ->InMemoryTable.Entity.dropCommittedChanges(~committedCheckpointId, ~keepLoadedFromDb)
193
232
  )
194
233
  state
195
- ->IndexerState.effects
196
- ->Utils.Dict.forEach(inMemTable =>
234
+ ->IndexerState.effectState
235
+ ->EffectState.forEach(inMemTable =>
197
236
  inMemTable->InMemoryStore.dropCommittedEffects(~committedCheckpointId, ~keepLoadedFromDb)
198
237
  )
199
238
  }