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
@@ -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
@@ -192,8 +208,8 @@ let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
192
208
  ->InMemoryTable.Entity.dropCommittedChanges(~committedCheckpointId, ~keepLoadedFromDb)
193
209
  )
194
210
  state
195
- ->IndexerState.effects
196
- ->Utils.Dict.forEach(inMemTable =>
211
+ ->IndexerState.effectState
212
+ ->EffectState.forEach(inMemTable =>
197
213
  inMemTable->InMemoryStore.dropCommittedEffects(~committedCheckpointId, ~keepLoadedFromDb)
198
214
  )
199
215
  }
@@ -2,8 +2,10 @@
2
2
 
3
3
  import * as Env from "./Env.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
+ import * as Internal from "./Internal.res.mjs";
5
6
  import * as Throttler from "./Throttler.res.mjs";
6
7
  import * as Prometheus from "./Prometheus.res.mjs";
8
+ import * as EffectState from "./EffectState.res.mjs";
7
9
  import * as IndexerState from "./IndexerState.res.mjs";
8
10
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
11
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
@@ -20,7 +22,7 @@ function getChangesCount(state) {
20
22
  IndexerState.allEntities(state).forEach(entityConfig => {
21
23
  total.contents = total.contents + InMemoryStore.getInMemTable(state, entityConfig).changesCount;
22
24
  });
23
- Utils.Dict.forEach(IndexerState.effects(state), inMemTable => {
25
+ EffectState.forEach(IndexerState.effectState(state), inMemTable => {
24
26
  total.contents = total.contents + inMemTable.changesCount;
25
27
  });
26
28
  IndexerState.processedBatches(state).forEach(batch => {
@@ -35,10 +37,12 @@ function waitForCommit(state) {
35
37
 
36
38
  function snapshotEffects(state, cache) {
37
39
  let acc = [];
38
- Utils.Dict.forEach(IndexerState.effects(state), inMemTable => {
40
+ EffectState.forEach(IndexerState.effectState(state), inMemTable => {
39
41
  let idsToStore = inMemTable.idsToStore;
40
42
  let invalidationsCount = inMemTable.invalidationsCount;
41
43
  let dict = inMemTable.dict;
44
+ let table = inMemTable.table;
45
+ let scope = inMemTable.scope;
42
46
  let effect = inMemTable.effect;
43
47
  if (idsToStore.length !== 0) {
44
48
  let items = Stdlib_Array.filterMap(idsToStore, id => {
@@ -51,23 +55,27 @@ function snapshotEffects(state, cache) {
51
55
  }
52
56
  });
53
57
  let effectName = effect.name;
54
- let c = cache[effectName];
58
+ let tableName = table.tableName;
59
+ let c = cache[tableName];
55
60
  let effectCacheRecord;
56
61
  if (c !== undefined) {
57
62
  effectCacheRecord = c;
58
63
  } else {
59
64
  let c$1 = {
60
65
  effectName: effectName,
66
+ scope: scope,
67
+ tableName: tableName,
61
68
  count: 0
62
69
  };
63
- cache[effectName] = c$1;
70
+ cache[tableName] = c$1;
64
71
  effectCacheRecord = c$1;
65
72
  }
66
73
  let shouldInitialize = effectCacheRecord.count === 0;
67
74
  effectCacheRecord.count = (effectCacheRecord.count + items.length | 0) - invalidationsCount | 0;
68
- Prometheus.EffectCacheCount.set(effectCacheRecord.count, effectName);
75
+ Prometheus.EffectCacheCount.set(effectCacheRecord.count, effectName, Internal.EffectCache.scopeToString(scope));
69
76
  acc.push({
70
- effect: effect,
77
+ table: table,
78
+ itemSchema: effect.storageMeta.itemSchema,
71
79
  items: items,
72
80
  shouldInitialize: shouldInitialize
73
81
  });
@@ -164,7 +172,7 @@ function commitBatch(state, batch) {
164
172
  function dropCommitted(state, keepLoadedFromDb) {
165
173
  let committedCheckpointId = IndexerState.committedCheckpointId(state);
166
174
  IndexerState.allEntities(state).forEach(entityConfig => InMemoryTable.Entity.dropCommittedChanges(InMemoryStore.getInMemTable(state, entityConfig), committedCheckpointId, keepLoadedFromDb));
167
- Utils.Dict.forEach(IndexerState.effects(state), inMemTable => InMemoryStore.dropCommittedEffects(inMemTable, committedCheckpointId, keepLoadedFromDb));
175
+ EffectState.forEach(IndexerState.effectState(state), inMemTable => InMemoryStore.dropCommittedEffects(inMemTable, committedCheckpointId, keepLoadedFromDb));
168
176
  }
169
177
 
170
178
  async function awaitCapacity(state) {
@@ -173,5 +173,10 @@ module Fs = {
173
173
 
174
174
  @module("fs") @scope("promises")
175
175
  external readdir: Path.t => promise<array<string>> = "readdir"
176
+
177
+ type stats
178
+ @module("fs") @scope("promises")
179
+ external stat: Path.t => promise<stats> = "stat"
180
+ @send external statsIsDirectory: stats => bool = "isDirectory"
176
181
  }
177
182
  }
@@ -488,6 +488,16 @@ let make = (
488
488
 
489
489
  switch maybeConfig {
490
490
  | None => ()
491
+ // Exclude instructions from failed transactions. HyperSync has no
492
+ // server-side predicate to filter instructions by parent-transaction
493
+ // success (`InstructionSelection` only exposes `is_inner`, and
494
+ // instruction/transaction selections union at block level rather than
495
+ // joining), so we filter client-side on the `isCommitted` flag HyperSync
496
+ // already delivers on every instruction row (a required column, zero extra
497
+ // bandwidth). When HyperSync adds a server-side `is_committed` predicate the
498
+ // query can push this down and the client-side check becomes a redundant
499
+ // safety net.
500
+ | Some(_) if !instr.isCommitted => ()
491
501
  | Some(onEventRegistration) =>
492
502
  let eventConfig =
493
503
  onEventRegistration.eventConfig->(
@@ -415,7 +415,7 @@ function make(param) {
415
415
  let byteLengths = Stdlib_Option.getOr(orderingByProgram[instr.programId], []);
416
416
  let contractAddress = instr.programId;
417
417
  let maybeConfig = probeRouter(eventRouter, programId, instr, byteLengths, contractAddress, contractNameByAddress);
418
- if (maybeConfig !== undefined) {
418
+ if (maybeConfig !== undefined && instr.isCommitted) {
419
419
  let eventConfig = maybeConfig.eventConfig;
420
420
  let logKey = instr.slot.toString() + ":" + instr.transactionIndex.toString() + ":" + serializeInstructionAddress(instr.instructionAddress);
421
421
  let maybeLogs = Stdlib_Option.map(logsByKey[logKey], logs => logs.map(log => ({