envio 3.5.0 → 3.5.1-svm-alpha.2

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 (59) hide show
  1. package/evm.schema.json +7 -0
  2. package/fuel.schema.json +7 -0
  3. package/index.d.ts +33 -10
  4. package/package.json +6 -6
  5. package/src/ChainFetching.res +11 -21
  6. package/src/ChainFetching.res.mjs +12 -20
  7. package/src/ChainState.res +41 -4
  8. package/src/ChainState.res.mjs +39 -5
  9. package/src/ChainState.resi +10 -1
  10. package/src/Config.res +62 -3
  11. package/src/Config.res.mjs +49 -13
  12. package/src/EffectState.res +2 -2
  13. package/src/EffectState.res.mjs +2 -2
  14. package/src/EntityTables.res +32 -0
  15. package/src/EntityTables.res.mjs +44 -0
  16. package/src/Envio.res +7 -7
  17. package/src/Envio.res.mjs +5 -6
  18. package/src/FetchState.res +38 -75
  19. package/src/FetchState.res.mjs +20 -62
  20. package/src/Hasura.res +37 -3
  21. package/src/Hasura.res.mjs +23 -5
  22. package/src/InMemoryStore.res +95 -43
  23. package/src/InMemoryStore.res.mjs +69 -38
  24. package/src/IndexerState.res +30 -24
  25. package/src/IndexerState.res.mjs +20 -35
  26. package/src/IndexerState.resi +8 -6
  27. package/src/Internal.res +29 -18
  28. package/src/Internal.res.mjs +27 -10
  29. package/src/LoadLayer.res +39 -8
  30. package/src/LoadLayer.res.mjs +36 -9
  31. package/src/LoadLayer.resi +2 -0
  32. package/src/Persistence.res +12 -1
  33. package/src/PgStorage.res +183 -37
  34. package/src/PgStorage.res.mjs +149 -34
  35. package/src/PruneStaleHistory.res +1 -0
  36. package/src/PruneStaleHistory.res.mjs +2 -1
  37. package/src/Sink.res +3 -3
  38. package/src/Sink.res.mjs +2 -2
  39. package/src/TestIndexer.res +128 -17
  40. package/src/TestIndexer.res.mjs +75 -9
  41. package/src/UserContext.res +19 -4
  42. package/src/UserContext.res.mjs +15 -9
  43. package/src/Writing.res +8 -16
  44. package/src/Writing.res.mjs +10 -10
  45. package/src/bindings/ClickHouse.res +40 -4
  46. package/src/bindings/ClickHouse.res.mjs +37 -5
  47. package/src/bindings/Vitest.res +0 -3
  48. package/src/bindings/Vitest.res.mjs +2 -12
  49. package/src/db/EntityHistory.res +64 -21
  50. package/src/db/EntityHistory.res.mjs +43 -22
  51. package/src/db/InternalTable.res.mjs +31 -31
  52. package/src/db/Table.res +24 -1
  53. package/src/db/Table.res.mjs +26 -4
  54. package/src/sources/AddressStore.res +77 -26
  55. package/src/sources/AddressStore.res.mjs +21 -10
  56. package/src/sources/SvmHyperSyncClient.res +8 -7
  57. package/src/sources/SvmHyperSyncSource.res +38 -1
  58. package/src/sources/SvmHyperSyncSource.res.mjs +24 -0
  59. package/svm.schema.json +17 -4
@@ -2,11 +2,45 @@
2
2
  // mutations route through IndexerState's domain operations; the write loop and
3
3
  // capacity/flush coordination live in Writing.
4
4
 
5
+ // The scope must match the entity's own: a cross-chain entity has one table on
6
+ // the indexer, a per-chain entity one table per ChainState. Taking the scope
7
+ // rather than an optional chain id keeps a dummy chain id unrepresentable.
5
8
  let getInMemTable = (
6
9
  state: IndexerState.t,
7
10
  ~entityConfig: Internal.entityConfig,
11
+ ~scope: Internal.chainScope,
8
12
  ): InMemoryTable.Entity.t =>
9
- state->IndexerState.entities->IndexerState.EntityTables.get(~entityName=entityConfig.name)
13
+ switch scope {
14
+ | CrossChain => state->IndexerState.entities
15
+ | Chain(chainId) => state->IndexerState.getChainState(~chainId)->ChainState.entities
16
+ }->EntityTables.get(~entityName=entityConfig.name)
17
+
18
+ // The scope a given entity's rows live in when reached from a handler running
19
+ // on `chainId`.
20
+ let entityScope = (entityConfig: Internal.entityConfig, ~chainId): Internal.chainScope =>
21
+ entityConfig.crossChain ? CrossChain : Chain(chainId)
22
+
23
+ // The chain a row loaded from storage belongs to, taken off the row so what's
24
+ // left matches the entity schema the handlers see. A per-chain entity whose row
25
+ // carries no chain id would silently land in the wrong partition, so it throws.
26
+ let takeRowScope = (
27
+ entity: Internal.entity,
28
+ ~entityConfig: Internal.entityConfig,
29
+ ): Internal.chainScope =>
30
+ switch entityConfig.table->Table.getChainIdField {
31
+ | None => CrossChain
32
+ | Some(field) =>
33
+ let row = entity->(Utils.magic: Internal.entity => dict<ChainId.t>)
34
+ switch row->Utils.Dict.dangerouslyGetNonOption(field.fieldName) {
35
+ | Some(chainId) =>
36
+ row->Utils.Dict.deleteInPlace(field.fieldName)
37
+ Chain(chainId)
38
+ | None =>
39
+ JsError.throwWithMessage(
40
+ `Rollback row for the per-chain entity "${entityConfig.name}" with id "${entity.id}" is missing its "${field.fieldName}" value.`,
41
+ )
42
+ }
43
+ }
10
44
 
11
45
  let getEffectInMemTable = (
12
46
  state: IndexerState.t,
@@ -114,16 +148,16 @@ let prepareRollbackDiff = async (
114
148
  let _ = await persistence.allEntities
115
149
  ->Array.filter(entityConfig => entityConfig.storage.postgres)
116
150
  ->Array.map(async entityConfig => {
117
- let entityTable = state->getInMemTable(~entityConfig)
118
-
119
- let (removedIds, restoredEntitiesResult) = await persistence.storage.getRollbackData(
151
+ let (removals, restoredEntitiesResult) = await persistence.storage.getRollbackData(
120
152
  ~entityConfig,
121
153
  ~rollbackTargetCheckpointId,
122
154
  )
123
155
 
124
- removedIds->Array.forEach(entityId => {
156
+ removals->Array.forEach(({entityId, scope}: Persistence.rollbackRemoval) => {
125
157
  deletedEntities->Utils.Dict.push(entityConfig.name, entityId)
126
- entityTable->InMemoryTable.Entity.set(
158
+ state
159
+ ->getInMemTable(~entityConfig, ~scope)
160
+ ->InMemoryTable.Entity.set(
127
161
  ~committedCheckpointId,
128
162
  Delete({
129
163
  entityId,
@@ -138,8 +172,11 @@ let prepareRollbackDiff = async (
138
172
  ->(Utils.magic: array<unknown> => array<Internal.entity>)
139
173
 
140
174
  restoredEntities->Array.forEach((entity: Internal.entity) => {
175
+ let scope = entity->takeRowScope(~entityConfig)
141
176
  setEntities->Utils.Dict.push(entityConfig.name, entity.id)
142
- entityTable->InMemoryTable.Entity.set(
177
+ state
178
+ ->getInMemTable(~entityConfig, ~scope)
179
+ ->InMemoryTable.Entity.set(
143
180
  ~committedCheckpointId,
144
181
  Set({
145
182
  entityId: entity.id->EntityId.unsafeOfString,
@@ -157,46 +194,61 @@ let prepareRollbackDiff = async (
157
194
  }
158
195
  }
159
196
 
197
+ // Stages the addresses registered by this batch's events for the write. They're
198
+ // drained from each chain's address store rather than carried on the items that
199
+ // registered them: the store is where they already live, and it knows which
200
+ // ones the database hasn't seen yet.
160
201
  let setBatchDcs = (state: IndexerState.t, ~batch: Batch.t) => {
161
- let inMemTable = state->getInMemTable(~entityConfig=InternalTable.EnvioAddresses.entityConfig)
202
+ let inMemTable = state->getInMemTable(
203
+ ~entityConfig=InternalTable.EnvioAddresses.entityConfig,
204
+ ~scope=CrossChain,
205
+ )
162
206
  let committedCheckpointId = state->IndexerState.committedCheckpointId
163
207
 
164
- let itemIdx = ref(0)
165
-
166
- for checkpoint in 0 to batch.checkpointIds->Array.length - 1 {
167
- let checkpointId = batch.checkpointIds->Array.getUnsafe(checkpoint)
168
- let chainId = batch.checkpointChainIds->Array.getUnsafe(checkpoint)
169
- let checkpointEventsProcessed = batch.checkpointEventsProcessed->Array.getUnsafe(checkpoint)
170
-
171
- for idx in 0 to checkpointEventsProcessed - 1 {
172
- let item = batch.items->Array.getUnsafe(itemIdx.contents + idx)
173
- switch item->Internal.getItemDcs {
174
- | None => ()
175
- | Some(dcs) =>
176
- // Currently only events support contract registration, so we can cast to event item
177
- let eventItem = item->Internal.castUnsafeEventItem
178
- for dcIdx in 0 to dcs->Array.length - 1 {
179
- let dc = dcs->Array.getUnsafe(dcIdx)
180
- let entity: InternalTable.EnvioAddresses.t = {
181
- id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address),
182
- chainId,
183
- contractName: dc.contractName,
184
- registrationBlock: eventItem.blockNumber,
185
- registrationLogIndex: eventItem.logIndex,
186
- }
187
-
188
- inMemTable->InMemoryTable.Entity.set(
189
- ~committedCheckpointId,
190
- Set({
191
- entityId: entity.id->EntityId.unsafeOfString,
192
- checkpointId,
193
- entity: entity->InternalTable.EnvioAddresses.castToInternal,
194
- }),
195
- )
208
+ batch.progressedChainsById->Utils.Dict.forEach(progressedChain => {
209
+ let chainId = progressedChain.fetchState.chainId
210
+ let chainState = state->IndexerState.getChainState(~chainId)
211
+
212
+ // Most batches register nothing, so the checkpoints are only collected for a
213
+ // chain that has something waiting.
214
+ if chainState->ChainState.hasAddressesToWrite {
215
+ // The store pairs each drained registration with a checkpoint of its own
216
+ // chain, so only this chain's checkpoints go in — and the index it returns
217
+ // points back into these two parallel arrays.
218
+ let checkpointIds = []
219
+ let checkpointBlockNumbers = []
220
+ for idx in 0 to batch.checkpointIds->Array.length - 1 {
221
+ if batch.checkpointChainIds->Array.getUnsafe(idx) === chainId {
222
+ checkpointIds->Array.push(batch.checkpointIds->Array.getUnsafe(idx))
223
+ checkpointBlockNumbers->Array.push(batch.checkpointBlockNumbers->Array.getUnsafe(idx))
196
224
  }
197
225
  }
198
- }
199
226
 
200
- itemIdx := itemIdx.contents + checkpointEventsProcessed
201
- }
227
+ chainState
228
+ ->ChainState.drainAddressesForWrite(
229
+ ~toBlockInclusive=progressedChain.progressBlockNumber,
230
+ ~checkpointBlockNumbers,
231
+ )
232
+ ->Array.forEach(dc => {
233
+ let entity: InternalTable.EnvioAddresses.t = {
234
+ id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address),
235
+ chainId,
236
+ contractName: dc.contractName,
237
+ registrationBlock: dc.registrationBlock,
238
+ // Only ever written, never read back. Kept on the table so the column
239
+ // doesn't need a migration.
240
+ registrationLogIndex: -1,
241
+ }
242
+
243
+ inMemTable->InMemoryTable.Entity.set(
244
+ ~committedCheckpointId,
245
+ Set({
246
+ entityId: entity.id->EntityId.unsafeOfString,
247
+ checkpointId: checkpointIds->Array.getUnsafe(dc.checkpointIdx),
248
+ entity: entity->InternalTable.EnvioAddresses.castToInternal,
249
+ }),
250
+ )
251
+ })
252
+ }
253
+ })
202
254
  }
@@ -4,14 +4,42 @@ import * as Table from "./db/Table.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
5
  import * as Config from "./Config.res.mjs";
6
6
  import * as Internal from "./Internal.res.mjs";
7
+ import * as ChainState from "./ChainState.res.mjs";
7
8
  import * as EffectState from "./EffectState.res.mjs";
9
+ import * as EntityTables from "./EntityTables.res.mjs";
8
10
  import * as IndexerState from "./IndexerState.res.mjs";
9
11
  import * as InMemoryTable from "./InMemoryTable.res.mjs";
10
12
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
13
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
14
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
15
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
12
16
 
13
- function getInMemTable(state, entityConfig) {
14
- return IndexerState.EntityTables.get(IndexerState.entities(state), entityConfig.name);
17
+ function getInMemTable(state, entityConfig, scope) {
18
+ let tmp;
19
+ tmp = scope === "crossChain" ? IndexerState.entities(state) : ChainState.entities(IndexerState.getChainState(state, scope));
20
+ return EntityTables.get(tmp, entityConfig.name);
21
+ }
22
+
23
+ function entityScope(entityConfig, chainId) {
24
+ if (entityConfig.crossChain) {
25
+ return "crossChain";
26
+ } else {
27
+ return chainId;
28
+ }
29
+ }
30
+
31
+ function takeRowScope(entity, entityConfig) {
32
+ let field = Table.getChainIdField(entityConfig.table);
33
+ if (field === undefined) {
34
+ return "crossChain";
35
+ }
36
+ let chainId = entity[field.fieldName];
37
+ if (chainId !== undefined) {
38
+ Utils.Dict.deleteInPlace(entity, field.fieldName);
39
+ return Primitive_option.valFromOption(chainId);
40
+ } else {
41
+ return Stdlib_JsError.throwWithMessage(`Rollback row for the per-chain entity "` + entityConfig.name + `" with id "` + entity.id + `" is missing its "` + field.fieldName + `" value.`);
42
+ }
15
43
  }
16
44
 
17
45
  function getEffectInMemTable(state, effect, scope) {
@@ -87,11 +115,11 @@ async function prepareRollbackDiff(state, rollbackTargetCheckpointId, rollbackDi
87
115
  let deletedEntities = {};
88
116
  let setEntities = {};
89
117
  await Promise.all(persistence.allEntities.filter(entityConfig => entityConfig.storage.postgres).map(async entityConfig => {
90
- let entityTable = getInMemTable(state, entityConfig);
91
118
  let match = await persistence.storage.getRollbackData(entityConfig, rollbackTargetCheckpointId);
92
- match[0].forEach(entityId => {
119
+ match[0].forEach(param => {
120
+ let entityId = param.entityId;
93
121
  Utils.Dict.push(deletedEntities, entityConfig.name, entityId);
94
- InMemoryTable.Entity.set(entityTable, committedCheckpointId, {
122
+ InMemoryTable.Entity.set(getInMemTable(state, entityConfig, param.scope), committedCheckpointId, {
95
123
  type: "DELETE",
96
124
  entityId: entityId,
97
125
  checkpointId: rollbackDiffCheckpointId
@@ -99,8 +127,9 @@ async function prepareRollbackDiff(state, rollbackTargetCheckpointId, rollbackDi
99
127
  });
100
128
  let restoredEntities = S$RescriptSchema.parseOrThrow(match[1], Table.pgRowsSchema(entityConfig.table));
101
129
  restoredEntities.forEach(entity => {
130
+ let scope = takeRowScope(entity, entityConfig);
102
131
  Utils.Dict.push(setEntities, entityConfig.name, entity.id);
103
- InMemoryTable.Entity.set(entityTable, committedCheckpointId, {
132
+ InMemoryTable.Entity.set(getInMemTable(state, entityConfig, scope), committedCheckpointId, {
104
133
  type: "SET",
105
134
  entityId: entity.id,
106
135
  entity: entity,
@@ -115,45 +144,47 @@ async function prepareRollbackDiff(state, rollbackTargetCheckpointId, rollbackDi
115
144
  }
116
145
 
117
146
  function setBatchDcs(state, batch) {
118
- let inMemTable = getInMemTable(state, Config.EnvioAddresses.entityConfig);
147
+ let inMemTable = getInMemTable(state, Config.EnvioAddresses.entityConfig, "crossChain");
119
148
  let committedCheckpointId = IndexerState.committedCheckpointId(state);
120
- let itemIdx = 0;
121
- for (let checkpoint = 0, checkpoint_finish = batch.checkpointIds.length; checkpoint < checkpoint_finish; ++checkpoint) {
122
- let checkpointId = batch.checkpointIds[checkpoint];
123
- let chainId = batch.checkpointChainIds[checkpoint];
124
- let checkpointEventsProcessed = batch.checkpointEventsProcessed[checkpoint];
125
- for (let idx = 0; idx < checkpointEventsProcessed; ++idx) {
126
- let item = batch.items[itemIdx + idx | 0];
127
- let dcs = item.dcs;
128
- if (dcs !== undefined) {
129
- for (let dcIdx = 0, dcIdx_finish = dcs.length; dcIdx < dcIdx_finish; ++dcIdx) {
130
- let dc = dcs[dcIdx];
131
- let entity_id = Config.EnvioAddresses.makeId(chainId, dc.address);
132
- let entity_registration_block = item.blockNumber;
133
- let entity_registration_log_index = item.logIndex;
134
- let entity_contract_name = dc.contractName;
135
- let entity = {
136
- id: entity_id,
137
- chain_id: chainId,
138
- registration_block: entity_registration_block,
139
- registration_log_index: entity_registration_log_index,
140
- contract_name: entity_contract_name
141
- };
142
- InMemoryTable.Entity.set(inMemTable, committedCheckpointId, {
143
- type: "SET",
144
- entityId: entity_id,
145
- entity: entity,
146
- checkpointId: checkpointId
147
- });
148
- }
149
+ Utils.Dict.forEach(batch.progressedChainsById, progressedChain => {
150
+ let chainId = progressedChain.fetchState.chainId;
151
+ let chainState = IndexerState.getChainState(state, chainId);
152
+ if (!ChainState.hasAddressesToWrite(chainState)) {
153
+ return;
154
+ }
155
+ let checkpointIds = [];
156
+ let checkpointBlockNumbers = [];
157
+ for (let idx = 0, idx_finish = batch.checkpointIds.length; idx < idx_finish; ++idx) {
158
+ if (batch.checkpointChainIds[idx] === chainId) {
159
+ checkpointIds.push(batch.checkpointIds[idx]);
160
+ checkpointBlockNumbers.push(batch.checkpointBlockNumbers[idx]);
149
161
  }
150
162
  }
151
- itemIdx = itemIdx + checkpointEventsProcessed | 0;
152
- }
163
+ ChainState.drainAddressesForWrite(chainState, progressedChain.progressBlockNumber, checkpointBlockNumbers).forEach(dc => {
164
+ let entity_id = Config.EnvioAddresses.makeId(chainId, dc.address);
165
+ let entity_registration_block = dc.registrationBlock;
166
+ let entity_contract_name = dc.contractName;
167
+ let entity = {
168
+ id: entity_id,
169
+ chain_id: chainId,
170
+ registration_block: entity_registration_block,
171
+ registration_log_index: -1,
172
+ contract_name: entity_contract_name
173
+ };
174
+ InMemoryTable.Entity.set(inMemTable, committedCheckpointId, {
175
+ type: "SET",
176
+ entityId: entity_id,
177
+ entity: entity,
178
+ checkpointId: checkpointIds[dc.checkpointIdx]
179
+ });
180
+ });
181
+ });
153
182
  }
154
183
 
155
184
  export {
156
185
  getInMemTable,
186
+ entityScope,
187
+ takeRowScope,
157
188
  getEffectInMemTable,
158
189
  hasEffectOutput,
159
190
  getEffectOutputUnsafe,
@@ -5,28 +5,6 @@ type rollbackState =
5
5
  | FoundReorgDepth({chainId: ChainId.t, rollbackTargetBlockNumber: int})
6
6
  | RollbackReady({eventsProcessedDiffByChain: dict<float>})
7
7
 
8
- module EntityTables = {
9
- type t = dict<InMemoryTable.Entity.t>
10
- exception UndefinedEntity({entityName: string})
11
- let make = (entities: array<Internal.entityConfig>): t => {
12
- let init = Dict.make()
13
- entities->Array.forEach(entityConfig => {
14
- init->Dict.set((entityConfig.name :> string), InMemoryTable.Entity.make())
15
- })
16
- init
17
- }
18
-
19
- let get = (self: t, ~entityName: string) => {
20
- switch self->Utils.Dict.dangerouslyGetNonOption(entityName) {
21
- | Some(table) => table
22
- | None =>
23
- UndefinedEntity({entityName: entityName})->ErrorHandling.mkLogAndRaise(
24
- ~msg="Unexpected, entity InMemoryTable is undefined",
25
- )
26
- }
27
- }
28
- }
29
-
30
8
  // Per-(contract, event) handler counters rendered into the
31
9
  // envio_processing_handler_* and envio_preload_handler_* metrics.
32
10
  type handlerStat = {
@@ -75,6 +53,8 @@ type t = {
75
53
  persistence: Persistence.t,
76
54
  // --- In-memory store: entity/effect tables and the pending-write queue. ---
77
55
  allEntities: array<Internal.entityConfig>,
56
+ // Cross-chain entities only; each ChainState holds its own partition for the
57
+ // per-chain ones.
78
58
  mutable entities: EntityTables.t,
79
59
  effectState: EffectState.t,
80
60
  mutable rollback: option<Persistence.rollback>,
@@ -186,7 +166,7 @@ let make = (
186
166
  config,
187
167
  persistence,
188
168
  allEntities: persistence.allEntities,
189
- entities: EntityTables.make(persistence.allEntities),
169
+ entities: EntityTables.make(persistence.allEntities->EntityTables.crossChain),
190
170
  effectState: EffectState.make(),
191
171
  rollback: None,
192
172
  committedCheckpointId,
@@ -467,6 +447,30 @@ let config = (state: t) => state.config
467
447
  let persistence = (state: t) => state.persistence
468
448
  let allEntities = (state: t) => state.allEntities
469
449
  let entities = (state: t) => state.entities
450
+
451
+ // Every in-memory entity table across all scopes, cross-chain first. The size,
452
+ // drop and flush passes walk the whole store this way instead of assuming a
453
+ // single partition.
454
+ let eachEntityTable = (state: t, fn: (~entityConfig: Internal.entityConfig, ~scope: Internal.chainScope, ~table: InMemoryTable.Entity.t) => unit) => {
455
+ let chainStates = state.crossChainState->CrossChainState.chainStates
456
+ state.allEntities->Array.forEach(entityConfig =>
457
+ if entityConfig.crossChain {
458
+ fn(
459
+ ~entityConfig,
460
+ ~scope=Internal.CrossChain,
461
+ ~table=state.entities->EntityTables.get(~entityName=entityConfig.name),
462
+ )
463
+ } else {
464
+ chainStates->Utils.Dict.forEach(chainState => {
465
+ fn(
466
+ ~entityConfig,
467
+ ~scope=Internal.Chain((chainState->ChainState.chainConfig).id),
468
+ ~table=chainState->ChainState.entities->EntityTables.get(~entityName=entityConfig.name),
469
+ )
470
+ })
471
+ }
472
+ )
473
+ }
470
474
  let effectState = (state: t) => state.effectState
471
475
  let committedCheckpointId = (state: t) => state.committedCheckpointId
472
476
  let processedCheckpointId = (state: t) => state.processedCheckpointId
@@ -802,7 +806,9 @@ let beginRollbackDiff = (
802
806
  ~diffCheckpointId,
803
807
  ~progressBlockNumberByChainId,
804
808
  ) => {
805
- state.entities = EntityTables.make(state.allEntities)
809
+ let perChainEntities = state.allEntities->EntityTables.perChain
810
+ state.entities = EntityTables.make(state.allEntities->EntityTables.crossChain)
811
+ state->chainStates->Utils.Dict.forEach(cs => cs->ChainState.resetEntities(~perChainEntities))
806
812
  state.effectState->EffectState.resetForRollback
807
813
  state.rollback = Some({
808
814
  targetCheckpointId,
@@ -12,40 +12,17 @@ import * as ChainState from "./ChainState.res.mjs";
12
12
  import * as EffectState from "./EffectState.res.mjs";
13
13
  import * as LoadManager from "./LoadManager.res.mjs";
14
14
  import * as Performance from "./bindings/Performance.res.mjs";
15
+ import * as EntityTables from "./EntityTables.res.mjs";
15
16
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
16
- import * as InMemoryTable from "./InMemoryTable.res.mjs";
17
17
  import * as SourceManager from "./sources/SourceManager.res.mjs";
18
18
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
19
19
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
20
20
  import * as CrossChainState from "./CrossChainState.res.mjs";
21
21
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
22
22
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
23
- import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
24
23
  import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
25
24
 
26
- let UndefinedEntity = /* @__PURE__ */Primitive_exceptions.create("IndexerState.EntityTables.UndefinedEntity");
27
-
28
- function make(entities) {
29
- let init = {};
30
- entities.forEach(entityConfig => {
31
- init[entityConfig.name] = InMemoryTable.Entity.make();
32
- });
33
- return init;
34
- }
35
-
36
- function get(self, entityName) {
37
- let table = self[entityName];
38
- if (table !== undefined) {
39
- return table;
40
- } else {
41
- return ErrorHandling.mkLogAndRaise(undefined, "Unexpected, entity InMemoryTable is undefined", {
42
- RE_EXN_ID: UndefinedEntity,
43
- entityName: entityName
44
- });
45
- }
46
- }
47
-
48
- function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt, committedCheckpointIdOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
25
+ function make(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt, committedCheckpointIdOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
49
26
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
50
27
  let committedCheckpointId = committedCheckpointIdOpt !== undefined ? committedCheckpointIdOpt : Internal.initialCheckpointId;
51
28
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
@@ -60,7 +37,7 @@ function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime
60
37
  config: config,
61
38
  persistence: persistence,
62
39
  allEntities: persistence.allEntities,
63
- entities: make(persistence.allEntities),
40
+ entities: EntityTables.make(EntityTables.crossChain(persistence.allEntities)),
64
41
  effectState: EffectState.make(),
65
42
  rollback: undefined,
66
43
  committedCheckpointId: committedCheckpointId,
@@ -125,7 +102,7 @@ function makeFromDbState(config, persistence, initialState, registrationsByChain
125
102
  let chainConfig = ChainMap.get(config.chainMap, chainId);
126
103
  chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrationsByChainId, reducedPollingInterval);
127
104
  });
128
- let state = make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSize, initialState.checkpointId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
105
+ let state = make(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSize, initialState.checkpointId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
129
106
  CrossChainState.markCaughtUpOnResume(state.crossChainState);
130
107
  Utils.Dict.forEach(initialState.cache, param => {
131
108
  let count = param.count;
@@ -288,6 +265,17 @@ function entities(state) {
288
265
  return state.entities;
289
266
  }
290
267
 
268
+ function eachEntityTable(state, fn) {
269
+ let chainStates = CrossChainState.chainStates(state.crossChainState);
270
+ state.allEntities.forEach(entityConfig => {
271
+ if (entityConfig.crossChain) {
272
+ return fn(entityConfig, "crossChain", EntityTables.get(state.entities, entityConfig.name));
273
+ } else {
274
+ return Utils.Dict.forEach(chainStates, chainState => fn(entityConfig, ChainState.chainConfig(chainState).id, EntityTables.get(ChainState.entities(chainState), entityConfig.name)));
275
+ }
276
+ });
277
+ }
278
+
291
279
  function effectState(state) {
292
280
  return state.effectState;
293
281
  }
@@ -678,7 +666,9 @@ function markCommitted(state, upToCheckpointId) {
678
666
  }
679
667
 
680
668
  function beginRollbackDiff(state, targetCheckpointId, diffCheckpointId, progressBlockNumberByChainId) {
681
- state.entities = make(state.allEntities);
669
+ let perChainEntities = EntityTables.perChain(state.allEntities);
670
+ state.entities = EntityTables.make(EntityTables.crossChain(state.allEntities));
671
+ Utils.Dict.forEach(CrossChainState.chainStates(state.crossChainState), cs => ChainState.resetEntities(cs, perChainEntities));
682
672
  EffectState.resetForRollback(state.effectState);
683
673
  state.rollback = {
684
674
  targetCheckpointId: targetCheckpointId,
@@ -745,16 +735,10 @@ function takeChainMetaSnapshot(state) {
745
735
  }
746
736
  }
747
737
 
748
- let EntityTables = {
749
- make: make,
750
- get: get
751
- };
752
-
753
738
  let unexpectedErrorMsg = "Indexer has failed with an unexpected error";
754
739
 
755
740
  export {
756
- EntityTables,
757
- make$1 as make,
741
+ make,
758
742
  makeFromDbState,
759
743
  unexpectedErrorMsg,
760
744
  isStale,
@@ -781,6 +765,7 @@ export {
781
765
  persistence,
782
766
  allEntities,
783
767
  entities,
768
+ eachEntityTable,
784
769
  effectState,
785
770
  committedCheckpointId,
786
771
  processedCheckpointId,
@@ -9,12 +9,6 @@ type rollbackState =
9
9
  | FoundReorgDepth({chainId: ChainId.t, rollbackTargetBlockNumber: int})
10
10
  | RollbackReady({eventsProcessedDiffByChain: dict<float>})
11
11
 
12
- module EntityTables: {
13
- type t = dict<InMemoryTable.Entity.t>
14
- let make: array<Internal.entityConfig> => t
15
- let get: (t, ~entityName: string) => InMemoryTable.Entity.t
16
- }
17
-
18
12
  type t
19
13
 
20
14
  let make: (
@@ -80,6 +74,14 @@ let config: t => Config.t
80
74
  let persistence: t => Persistence.t
81
75
  let allEntities: t => array<Internal.entityConfig>
82
76
  let entities: t => EntityTables.t
77
+ let eachEntityTable: (
78
+ t,
79
+ (
80
+ ~entityConfig: Internal.entityConfig,
81
+ ~scope: Internal.chainScope,
82
+ ~table: InMemoryTable.Entity.t,
83
+ ) => unit,
84
+ ) => unit
83
85
  let effectState: t => EffectState.t
84
86
  let committedCheckpointId: t => Internal.checkpointId
85
87
  let processedCheckpointId: t => Internal.checkpointId
package/src/Internal.res CHANGED
@@ -616,8 +616,6 @@ type indexingAddress = {
616
616
  registrationBlock: int,
617
617
  }
618
618
 
619
- type dcs = array<indexingAddress>
620
-
621
619
  // Duplicate the type from item to keep item properly unboxed. Runtime event
622
620
  // items carry the registration their source already resolved from the
623
621
  // ChainState-owned registration array.
@@ -698,11 +696,6 @@ let getItemChainId = item =>
698
696
  | Block({onBlockRegistration: {chainId}}) => chainId
699
697
  }
700
698
 
701
- @get
702
- external getItemDcs: item => option<dcs> = "dcs"
703
- @set
704
- external setItemDcs: (item, dcs) => unit = "dcs"
705
-
706
699
  type eventOptions<'where> = {
707
700
  wildcard?: bool,
708
701
  where?: 'where,
@@ -751,6 +744,10 @@ type genericEntityConfig<'entity> = {
751
744
  schema: S.t<'entity>,
752
745
  table: Table.table,
753
746
  storage: entityStorage,
747
+ // Resolved by the CLI against the config's `defaultCrossChain` and the
748
+ // entity's `@crossChain`. When false the table carries a chain-id column in
749
+ // its primary key and every row belongs to exactly one chain.
750
+ crossChain: bool,
754
751
  }
755
752
  type entityConfig = genericEntityConfig<entity>
756
753
  external fromGenericEntityConfig: genericEntityConfig<'entity> => entityConfig = "%identity"
@@ -780,10 +777,11 @@ type effect = {
780
777
  handler: effectArgs => promise<effectOutput>,
781
778
  storageMeta: effectCacheStorageMeta,
782
779
  defaultShouldCache: bool,
783
- // When true (the default) a single cache is shared across every chain and the
784
- // handler must not read context.chain. When false the cache is isolated per
785
- // chain and context.chain.id is available.
786
- crossChain: bool,
780
+ // When true a single cache is shared across every chain and the handler must
781
+ // not read context.chain. When false the cache is isolated per chain and
782
+ // context.chain.id is available. None means the effect didn't say, and the
783
+ // config's `defaultCrossChain` decides.
784
+ crossChain: option<bool>,
787
785
  output: S.t<effectOutput>,
788
786
  input: S.t<effectInput>,
789
787
  rateLimit: option<rateLimitOptions>,
@@ -798,6 +796,26 @@ type chainScope =
798
796
  | @as("crossChain") CrossChain
799
797
  | Chain(ChainId.t)
800
798
 
799
+ let chainScopeToString = scope =>
800
+ switch scope {
801
+ | CrossChain => "crossChain"
802
+ | Chain(chainId) => chainId->ChainId.toString
803
+ }
804
+
805
+ let chainScopeChainId = scope =>
806
+ switch scope {
807
+ | CrossChain => None
808
+ | Chain(chainId) => Some(chainId)
809
+ }
810
+
811
+ // A copy of the entity carrying the chain-id column a per-chain entity's row
812
+ // needs. Never mutates the handler's object, which stays in the in-memory table.
813
+ let stampChainId = (entity: entity, ~fieldName, ~chainId: ChainId.t): entity =>
814
+ Utils.Dict.merge(
815
+ entity->(Utils.magic: entity => dict<unknown>),
816
+ Dict.fromArray([(fieldName, chainId->(Utils.magic: ChainId.t => unknown))]),
817
+ )->(Utils.magic: dict<unknown> => entity)
818
+
801
819
  let cacheTablePrefix = "envio_effect_"
802
820
 
803
821
  // The single reversible mapping between an effect's (name, scope) and its
@@ -813,13 +831,6 @@ module EffectCache = {
813
831
  | Chain(chainId) => `envio_${chainId->ChainId.toString}_effect_${effectName}`
814
832
  }
815
833
 
816
- // "crossChain" or the decimal chain id. Used as the `scope` Prometheus label.
817
- let scopeToString = scope =>
818
- switch scope {
819
- | CrossChain => "crossChain"
820
- | Chain(chainId) => chainId->ChainId.toString
821
- }
822
-
823
834
  // Only accepts a canonical decimal chain id ("7", not "007" or "1foo") —
824
835
  // the schema's parser follows parseFloat semantics and accepts both.
825
836
  let parseChainId = str =>