envio 3.5.0 → 3.5.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -69,10 +69,10 @@
69
69
  "tsx": "4.21.0"
70
70
  },
71
71
  "optionalDependencies": {
72
- "envio-linux-x64": "3.5.0",
73
- "envio-linux-x64-musl": "3.5.0",
74
- "envio-linux-arm64": "3.5.0",
75
- "envio-darwin-x64": "3.5.0",
76
- "envio-darwin-arm64": "3.5.0"
72
+ "envio-linux-x64": "3.5.1",
73
+ "envio-linux-x64-musl": "3.5.1",
74
+ "envio-linux-arm64": "3.5.1",
75
+ "envio-darwin-x64": "3.5.1",
76
+ "envio-darwin-arm64": "3.5.1"
77
77
  }
78
78
  }
@@ -24,25 +24,15 @@ let runContractRegistersOrThrow = async (
24
24
  ~ecosystem=config.ecosystem.name,
25
25
  )
26
26
 
27
- let itemsWithDcs = []
27
+ let registrations: array<AddressStore.registration> = []
28
28
 
29
29
  let onRegister = (~item: Internal.item, ~contractAddress, ~contractName) => {
30
30
  let eventItem = item->Internal.castUnsafeEventItem
31
- let {blockNumber} = eventItem
32
-
33
- let dc: Internal.indexingAddress = {
31
+ registrations->Array.push({
34
32
  address: contractAddress,
35
33
  contractName,
36
- registrationBlock: blockNumber,
37
- }
38
-
39
- switch item->Internal.getItemDcs {
40
- | None => {
41
- item->Internal.setItemDcs([dc])
42
- itemsWithDcs->Array.push(item)
43
- }
44
- | Some(dcs) => dcs->Array.push(dc)
45
- }
34
+ registrationBlock: eventItem.blockNumber,
35
+ })
46
36
  }
47
37
 
48
38
  let promises = []
@@ -101,7 +91,7 @@ let runContractRegistersOrThrow = async (
101
91
  let _ = await Promise.all(promises)
102
92
  }
103
93
 
104
- itemsWithDcs
94
+ registrations
105
95
  }
106
96
 
107
97
  let rec onQueryResponse = async (
@@ -226,13 +216,13 @@ let rec onQueryResponse = async (
226
216
 
227
217
  // Re-check staleness: contract registration is async, so the chain state
228
218
  // may have rolled back by the time we apply the fetched items.
229
- let proceed = (~newItemsWithDcs) =>
219
+ let proceed = (~newRegistrations) =>
230
220
  if !(state->IndexerState.isStale(~stateId)) {
231
221
  applyQueryResponse(
232
222
  state,
233
223
  ~chainId,
234
224
  ~newItems,
235
- ~newItemsWithDcs,
225
+ ~newRegistrations,
236
226
  ~knownHeight,
237
227
  ~latestFetchedBlock={
238
228
  FetchState.blockNumber: latestFetchedBlockNumber,
@@ -248,7 +238,7 @@ let rec onQueryResponse = async (
248
238
  }
249
239
 
250
240
  switch itemsWithContractRegister {
251
- | [] => proceed(~newItemsWithDcs=[])
241
+ | [] => proceed(~newRegistrations=[])
252
242
  | _ =>
253
243
  switch await runContractRegistersOrThrow(
254
244
  ~itemsWithContractRegister,
@@ -257,7 +247,7 @@ let rec onQueryResponse = async (
257
247
  ~blockStore,
258
248
  ) {
259
249
  | exception exn => IndexerState.errorExit(state, exn->ErrorHandling.make)
260
- | newItemsWithDcs => proceed(~newItemsWithDcs)
250
+ | newRegistrations => proceed(~newRegistrations)
261
251
  }
262
252
  }
263
253
  }
@@ -267,7 +257,7 @@ and applyQueryResponse = (
267
257
  state: IndexerState.t,
268
258
  ~chainId,
269
259
  ~newItems,
270
- ~newItemsWithDcs,
260
+ ~newRegistrations,
271
261
  ~knownHeight,
272
262
  ~latestFetchedBlock,
273
263
  ~query,
@@ -281,7 +271,7 @@ and applyQueryResponse = (
281
271
  ~query,
282
272
  ~latestFetchedBlock,
283
273
  ~newItems,
284
- ~newItemsWithDcs,
274
+ ~newRegistrations,
285
275
  ~knownHeight,
286
276
  ~transactionStore,
287
277
  ~blockStore,
@@ -18,21 +18,13 @@ import * as ContractRegisterContext from "./ContractRegisterContext.res.mjs";
18
18
 
19
19
  async function runContractRegistersOrThrow(itemsWithContractRegister, config, transactionStore, blockStore) {
20
20
  await ChainState.materializePageItems(itemsWithContractRegister, transactionStore, blockStore, config.ecosystem.name);
21
- let itemsWithDcs = [];
21
+ let registrations = [];
22
22
  let onRegister = (item, contractAddress, contractName) => {
23
- let dc_registrationBlock = item.blockNumber;
24
- let dc = {
23
+ registrations.push({
25
24
  address: contractAddress,
26
25
  contractName: contractName,
27
- registrationBlock: dc_registrationBlock
28
- };
29
- let dcs = item.dcs;
30
- if (dcs !== undefined) {
31
- dcs.push(dc);
32
- } else {
33
- item.dcs = [dc];
34
- itemsWithDcs.push(item);
35
- }
26
+ registrationBlock: item.blockNumber
27
+ });
36
28
  };
37
29
  let promises = [];
38
30
  for (let idx = 0, idx_finish = itemsWithContractRegister.length; idx < idx_finish; ++idx) {
@@ -68,13 +60,13 @@ async function runContractRegistersOrThrow(itemsWithContractRegister, config, tr
68
60
  if (Utils.$$Array.notEmpty(promises)) {
69
61
  await Promise.all(promises);
70
62
  }
71
- return itemsWithDcs;
63
+ return registrations;
72
64
  }
73
65
 
74
- function applyQueryResponse(state, chainId, newItems, newItemsWithDcs, knownHeight, latestFetchedBlock, query, transactionStore, blockStore) {
66
+ function applyQueryResponse(state, chainId, newItems, newRegistrations, knownHeight, latestFetchedBlock, query, transactionStore, blockStore) {
75
67
  let chainState = IndexerState.getChainState(state, chainId);
76
68
  let wasFetchingAtHead = ChainState.isFetchingAtHead(chainState);
77
- ChainState.handleQueryResult(chainState, query, newItems, newItemsWithDcs, latestFetchedBlock, knownHeight, transactionStore, blockStore);
69
+ ChainState.handleQueryResult(chainState, query, newItems, newRegistrations, latestFetchedBlock, knownHeight, transactionStore, blockStore);
78
70
  if (IndexerState.exitAfterFirstEventBlock(state) && newItems.length !== 0) {
79
71
  ChainState.setEndBlockToFirstEvent(chainState, newItems[0].blockNumber);
80
72
  }
@@ -152,9 +144,9 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
152
144
  itemsWithContractRegister.push(item);
153
145
  }
154
146
  }
155
- let proceed = newItemsWithDcs => {
147
+ let proceed = newRegistrations => {
156
148
  if (!IndexerState.isStale(state, stateId)) {
157
- applyQueryResponse(state, chainId, parsedQueueItems, newItemsWithDcs, knownHeight, {
149
+ applyQueryResponse(state, chainId, parsedQueueItems, newRegistrations, knownHeight, {
158
150
  blockNumber: latestFetchedBlockNumber,
159
151
  blockTimestamp: latestFetchedBlockTimestamp
160
152
  }, query, transactionStore, blockStore);
@@ -166,14 +158,14 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
166
158
  if (itemsWithContractRegister.length === 0) {
167
159
  return proceed([]);
168
160
  }
169
- let newItemsWithDcs;
161
+ let newRegistrations;
170
162
  try {
171
- newItemsWithDcs = await runContractRegistersOrThrow(itemsWithContractRegister, IndexerState.config(state), transactionStore, blockStore);
163
+ newRegistrations = await runContractRegistersOrThrow(itemsWithContractRegister, IndexerState.config(state), transactionStore, blockStore);
172
164
  } catch (raw_exn) {
173
165
  let exn = Primitive_exceptions.internalToException(raw_exn);
174
166
  return IndexerState.errorExit(state, ErrorHandling.make(exn, undefined, undefined));
175
167
  }
176
- return proceed(newItemsWithDcs);
168
+ return proceed(newRegistrations);
177
169
  }
178
170
 
179
171
  function finishWaitingForNewBlock(state, chainId, knownHeight, stateId, scheduleFetch, scheduleProcessing) {
@@ -130,6 +130,19 @@ let make = (
130
130
  }
131
131
  }
132
132
 
133
+ // Every chain's contracts, not just one chain's: `context.chain.X.add` accepts
134
+ // any config contract, whichever chain declared it, so every chain's address
135
+ // store has to hold the whole set.
136
+ let configContractNames = (config: Config.t) => {
137
+ let names = Utils.Set.make()
138
+ config.chainMap
139
+ ->ChainMap.values
140
+ ->Array.forEach(chain =>
141
+ chain.contracts->Array.forEach(contract => names->Utils.Set.add(contract.name)->ignore)
142
+ )
143
+ names->Utils.Set.toArray
144
+ }
145
+
133
146
  let makeInternal = (
134
147
  ~chainConfig: Config.chain,
135
148
  ~indexingAddresses: array<Internal.indexingAddress>,
@@ -174,7 +187,10 @@ let makeInternal = (
174
187
  let addressStore = AddressStore.make(
175
188
  ~ecosystem=config.ecosystem.name,
176
189
  ~shouldChecksum=!lowercaseAddresses,
177
- ~contracts=AddressStore.contractsOf(~onEventRegistrations),
190
+ ~contracts=AddressStore.contractsOf(
191
+ ~onEventRegistrations,
192
+ ~configContractNames=config->configContractNames,
193
+ ),
178
194
  )
179
195
 
180
196
  let fetchState = FetchState.make(
@@ -430,6 +446,15 @@ let setRollbackTargetBlock = (cs: t, ~blockNumber) => cs.rollbackTargetBlock = S
430
446
  let knownHeight = (cs: t) => cs.fetchState.knownHeight
431
447
  let contractAddresses = (cs: t, ~contractName) =>
432
448
  cs.addressStore->AddressStore.contractAddresses(contractName)
449
+
450
+ // Hands over the dynamically registered addresses the database hasn't seen yet,
451
+ // up to the block the caller is about to commit, each paired with the checkpoint
452
+ // that owns its row. Destructive: the caller must persist them or take the
453
+ // indexer down. Throws with nothing consumed when a registration's block has no
454
+ // checkpoint in the batch.
455
+ let drainAddressesForWrite = (cs: t, ~toBlockInclusive, ~checkpointBlockNumbers) =>
456
+ cs.addressStore->AddressStore.drainForWrite(toBlockInclusive, checkpointBlockNumbers)
457
+ let hasAddressesToWrite = (cs: t) => cs.addressStore->AddressStore.pendingCount > 0
433
458
  let bufferSize = (cs: t) => cs.fetchState->FetchState.bufferSize
434
459
  let bufferReadyCount = (cs: t) => cs.fetchState->FetchState.bufferReadyCount
435
460
  let getProgressPercentage = (cs: t) => cs.fetchState->FetchState.getProgressPercentage
@@ -841,7 +866,7 @@ let handleQueryResult = (
841
866
  cs: t,
842
867
  ~query: FetchState.query,
843
868
  ~newItems,
844
- ~newItemsWithDcs,
869
+ ~newRegistrations,
845
870
  ~latestFetchedBlock: FetchState.blockNumberAndTimestamp,
846
871
  ~knownHeight,
847
872
  ~transactionStore as txPage: option<TransactionStore.t>,
@@ -858,7 +883,7 @@ let handleQueryResult = (
858
883
  | None => ()
859
884
  }
860
885
 
861
- let fs = switch newItemsWithDcs {
886
+ let fs = switch newRegistrations {
862
887
  | [] => cs.fetchState
863
888
  | _ =>
864
889
  cs.fetchState->FetchState.registerDynamicContracts(
@@ -868,7 +893,7 @@ let handleQueryResult = (
868
893
  // catch-up range — and an unbounded query can reach past the height that
869
894
  // was known when it went out.
870
895
  ~claimCeiling=Pervasives.max(knownHeight, latestFetchedBlock.blockNumber),
871
- newItemsWithDcs,
896
+ newRegistrations,
872
897
  )
873
898
  }
874
899
 
@@ -6,6 +6,7 @@ import * as Batch from "./Batch.res.mjs";
6
6
  import * as Utils from "./Utils.res.mjs";
7
7
  import * as ChainId from "./ChainId.res.mjs";
8
8
  import * as Logging from "./Logging.res.mjs";
9
+ import * as ChainMap from "./ChainMap.res.mjs";
9
10
  import * as EvmChain from "./sources/EvmChain.res.mjs";
10
11
  import * as FieldMask from "./sources/FieldMask.res.mjs";
11
12
  import * as BlockStore from "./sources/BlockStore.res.mjs";
@@ -90,6 +91,16 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
90
91
  };
91
92
  }
92
93
 
94
+ function configContractNames(config) {
95
+ let names = new Set();
96
+ ChainMap.values(config.chainMap).forEach(chain => {
97
+ chain.contracts.forEach(contract => {
98
+ names.add(contract.name);
99
+ });
100
+ });
101
+ return Array.from(names);
102
+ }
103
+
93
104
  function makeInternal(chainConfig, indexingAddresses, startBlock, endBlock, firstEventBlockOpt, progressBlockNumber, config, registrationsByChainId, logger, timestampCaughtUpToHeadOrEndblock, numEventsProcessed, isInReorgThreshold, isRealtime, reorgCheckpoints, maxReorgDepth, knownHeightOpt, isResumedOpt, reducedPollingInterval) {
94
105
  let firstEventBlock = firstEventBlockOpt !== undefined ? Primitive_option.valFromOption(firstEventBlockOpt) : undefined;
95
106
  let knownHeight = knownHeightOpt !== undefined ? knownHeightOpt : 0;
@@ -106,7 +117,7 @@ function makeInternal(chainConfig, indexingAddresses, startBlock, endBlock, firs
106
117
  }
107
118
  });
108
119
  let lowercaseAddresses = config.lowercaseAddresses;
109
- let addressStore = AddressStore.make(config.ecosystem.name, !lowercaseAddresses, AddressStore.contractsOf(onEventRegistrations));
120
+ let addressStore = AddressStore.make(config.ecosystem.name, !lowercaseAddresses, AddressStore.contractsOf(onEventRegistrations, configContractNames(config)));
110
121
  let match$1 = config.ecosystem.name;
111
122
  let tmp;
112
123
  switch (match$1) {
@@ -259,6 +270,14 @@ function contractAddresses(cs, contractName) {
259
270
  return cs.addressStore.contractAddresses(contractName);
260
271
  }
261
272
 
273
+ function drainAddressesForWrite(cs, toBlockInclusive, checkpointBlockNumbers) {
274
+ return cs.addressStore.drainForWrite(toBlockInclusive, checkpointBlockNumbers);
275
+ }
276
+
277
+ function hasAddressesToWrite(cs) {
278
+ return cs.addressStore.pendingCount() > 0;
279
+ }
280
+
262
281
  function bufferSize(cs) {
263
282
  return FetchState.bufferSize(cs.fetchState);
264
283
  }
@@ -565,14 +584,14 @@ async function materializePageItems(items, transactionStore, blockStore, ecosyst
565
584
  ]);
566
585
  }
567
586
 
568
- function handleQueryResult(cs, query, newItems, newItemsWithDcs, latestFetchedBlock, knownHeight, txPage, blockPage) {
587
+ function handleQueryResult(cs, query, newItems, newRegistrations, latestFetchedBlock, knownHeight, txPage, blockPage) {
569
588
  if (txPage !== undefined) {
570
589
  cs.transactionStore.merge(Primitive_option.valFromOption(txPage));
571
590
  }
572
591
  if (blockPage !== undefined) {
573
592
  cs.blockStore.merge(Primitive_option.valFromOption(blockPage));
574
593
  }
575
- let fs = newItemsWithDcs.length !== 0 ? FetchState.registerDynamicContracts(cs.fetchState, cs.addressStore, Primitive_int.max(knownHeight, latestFetchedBlock.blockNumber), newItemsWithDcs) : cs.fetchState;
594
+ let fs = newRegistrations.length !== 0 ? FetchState.registerDynamicContracts(cs.fetchState, cs.addressStore, Primitive_int.max(knownHeight, latestFetchedBlock.blockNumber), newRegistrations) : cs.fetchState;
576
595
  cs.fetchState = FetchState.updateKnownHeight(FetchState.handleQueryResult(fs, query, latestFetchedBlock, newItems), knownHeight);
577
596
  cs.pendingBudget = Primitive_float.max(0, cs.pendingBudget - query.itemsEst);
578
597
  }
@@ -837,6 +856,8 @@ export {
837
856
  setRollbackTargetBlock,
838
857
  knownHeight,
839
858
  contractAddresses,
859
+ drainAddressesForWrite,
860
+ hasAddressesToWrite,
840
861
  bufferSize,
841
862
  bufferReadyCount,
842
863
  getProgressPercentage,
@@ -69,6 +69,12 @@ let setRollbackTargetBlock: (t, ~blockNumber: int) => unit
69
69
  // Fetch-frontier reads.
70
70
  let knownHeight: t => int
71
71
  let contractAddresses: (t, ~contractName: string) => array<Address.t>
72
+ let drainAddressesForWrite: (
73
+ t,
74
+ ~toBlockInclusive: int,
75
+ ~checkpointBlockNumbers: array<int>,
76
+ ) => array<AddressStore.drainedAddress>
77
+ let hasAddressesToWrite: t => bool
72
78
  let bufferSize: t => int
73
79
  let bufferReadyCount: t => int
74
80
  let getProgressPercentage: t => float
@@ -110,7 +116,7 @@ let handleQueryResult: (
110
116
  t,
111
117
  ~query: FetchState.query,
112
118
  ~newItems: array<Internal.item>,
113
- ~newItemsWithDcs: array<Internal.item>,
119
+ ~newRegistrations: array<AddressStore.registration>,
114
120
  ~latestFetchedBlock: FetchState.blockNumberAndTimestamp,
115
121
  ~knownHeight: int,
116
122
  ~transactionStore: option<TransactionStore.t>,
package/src/Config.res CHANGED
@@ -114,7 +114,8 @@ module EnvioAddresses = {
114
114
  id: string,
115
115
  @as("chain_id") chainId: ChainId.t,
116
116
  @as("registration_block") registrationBlock: int,
117
- // -1 when the address was registered from a block handler (no log index)
117
+ // Vestigial: always written as -1 and never read back. The column stays so
118
+ // an existing schema doesn't need a migration.
118
119
  @as("registration_log_index") registrationLogIndex: int,
119
120
  @as("contract_name") contractName: string,
120
121
  }
@@ -1473,7 +1473,7 @@ let warnRejectedRegistration = (
1473
1473
  ~params={"contractName": contractName},
1474
1474
  `Skipping contract registration: Not a valid address for this chain's ecosystem.`,
1475
1475
  )
1476
- | Added(_) | NoEvents(_) => ()
1476
+ | Added(_) => ()
1477
1477
  }
1478
1478
 
1479
1479
  /**
@@ -1678,9 +1678,9 @@ let registerDynamicContracts = (
1678
1678
  // height in here. Defaults to what the fetch state knows, which is all
1679
1679
  // there is to go on when no response is being applied.
1680
1680
  ~claimCeiling=fetchState.knownHeight,
1681
- // These are raw items which might have dynamic contracts received from contractRegister call.
1682
- // Might contain duplicates which we should filter out
1683
- items: array<Internal.item>,
1681
+ // Registrations collected from contractRegister calls, in the order the
1682
+ // handlers made them. May contain duplicates, which the store filters out.
1683
+ registrations: array<AddressStore.registration>,
1684
1684
  ) => {
1685
1685
  if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
1686
1686
  // Can the normalSelection be empty?
@@ -1689,91 +1689,42 @@ let registerDynamicContracts = (
1689
1689
  )
1690
1690
  }
1691
1691
 
1692
- // Flatten every item's dcs into one batch, remembering where each came from
1693
- // so the rejected ones can be spliced back out of their item (which is what
1694
- // keeps them from being persisted twice).
1695
- let registrations: array<AddressStore.registration> = []
1696
- let sourceDcs: array<Internal.dcs> = []
1697
- let sourceIndexes: array<int> = []
1698
- for itemIdx in 0 to items->Array.length - 1 {
1699
- switch items->Array.getUnsafe(itemIdx)->Internal.getItemDcs {
1700
- | None => ()
1701
- | Some(dcs) =>
1702
- for dcIdx in 0 to dcs->Array.length - 1 {
1703
- let dc = dcs->Array.getUnsafe(dcIdx)
1704
- registrations
1705
- ->Array.push({
1706
- address: dc.address,
1707
- contractName: dc.contractName,
1708
- registrationBlock: dc.registrationBlock,
1709
- })
1710
- ->ignore
1711
- sourceDcs->Array.push(dcs)->ignore
1712
- sourceIndexes->Array.push(dcIdx)->ignore
1713
- }
1714
- }
1715
- }
1716
-
1717
1692
  // Ids are handed out in registration order, so a cursor taken now selects
1718
1693
  // exactly what this batch adds.
1719
1694
  let idCursor = addressStore->AddressStore.nextId
1720
1695
  // The store resolves each address against both what it already holds and the
1721
1696
  // batch's own earlier entries, so two contracts claiming one address inside a
1722
- // single batch conflict the same way as across batches.
1697
+ // single batch conflict the same way as across batches. It also decides which
1698
+ // additions this chain fetches for, since it's what holds the contract list.
1723
1699
  let verdicts = addressStore->AddressStore.registerBatch(registrations)
1724
1700
 
1725
1701
  let registeringContractNames = []
1726
- let hasNoEventsUpdatesRef = ref(false)
1727
1702
  for idx in 0 to verdicts->Array.length - 1 {
1728
1703
  let registration = registrations->Array.getUnsafe(idx)
1729
1704
  let verdict = verdicts->Array.getUnsafe(idx)
1730
- let keep = switch verdict {
1731
- | Added(_) =>
1705
+ switch verdict {
1706
+ | Added({fetchable: true}) =>
1732
1707
  if !(registeringContractNames->Array.includes(registration.contractName)) {
1733
1708
  registeringContractNames->Array.push(registration.contractName)->ignore
1734
1709
  }
1735
- true
1736
- | NoEvents(_) =>
1737
- hasNoEventsUpdatesRef := true
1738
- // Persist the address to the db so a future config change that adds
1739
- // events for this contract can pick it up on restart, but skip partition
1740
- // registration since there's nothing to fetch.
1741
- warnAddressRegistration(
1742
- ~chainId=fetchState.chainId,
1743
- ~contractAddress=registration.address,
1744
- ~params={"contractName": registration.contractName},
1745
- `Persisting contract registration without fetching: Contract doesn't have any events to fetch. It'll be picked up on restart if you add events for the contract.`,
1746
- )
1747
- true
1710
+ // Nothing on this chain is fetched by address for the contract, so there's
1711
+ // no partition to build. The address is still stored and persisted, so a
1712
+ // config that later adds address-dependent events picks it up on restart.
1713
+ | Added({fetchable: false}) => ()
1748
1714
  | Conflict(_) | Duplicate(_) | Invalid =>
1749
1715
  verdict->warnRejectedRegistration(
1750
1716
  ~chainId=fetchState.chainId,
1751
1717
  ~contractAddress=registration.address,
1752
1718
  ~contractName=registration.contractName,
1753
1719
  )
1754
- false
1755
- }
1756
- if !keep {
1757
- // Mark for removal; the splice happens below, back to front, so earlier
1758
- // indexes stay valid.
1759
- sourceIndexes->Array.setUnsafe(idx, -1 - sourceIndexes->Array.getUnsafe(idx))
1760
- }
1761
- }
1762
- for idx in verdicts->Array.length - 1 downto 0 {
1763
- let marked = sourceIndexes->Array.getUnsafe(idx)
1764
- if marked < 0 {
1765
- let _ =
1766
- sourceDcs->Array.getUnsafe(idx)->Array.splice(~start=-1 - marked, ~remove=1, ~insert=[])
1767
1720
  }
1768
1721
  }
1769
1722
 
1770
- switch (registeringContractNames, hasNoEventsUpdatesRef.contents) {
1771
- // Dont update anything when everything was filtered out
1772
- | ([], false) => fetchState
1773
- // Only dcs for contracts without events. The store already tracks them so
1774
- // subsequent registrations see them, but there are no partitions to touch.
1775
- | ([], true) => fetchState
1776
- | (_, _) => {
1723
+ switch registeringContractNames {
1724
+ // Nothing to fetch: everything was rejected, or registered for contracts
1725
+ // this chain has no address-dependent events for.
1726
+ | [] => fetchState
1727
+ | _ => {
1777
1728
  let newPartitions = []
1778
1729
  let dynamicContractsRef = ref(fetchState.optimizedPartitions.dynamicContracts)
1779
1730
  let mutExistingPartitions = fetchState.optimizedPartitions.entities->Dict.valuesToArray
@@ -1835,7 +1786,11 @@ let registerDynamicContracts = (
1835
1786
  dynamicContract: Some(contractName),
1836
1787
  addresses: splitAddresses,
1837
1788
  mergeBlock: None,
1838
- mutPendingQueries: p.mutPendingQueries,
1789
+ // Must be a fresh array: sharing p's (empty) queue by
1790
+ // reference lets both split halves push and consume in
1791
+ // one array, so one half can consume the other's
1792
+ // fetched range and skip it for its own addresses.
1793
+ mutPendingQueries: [],
1839
1794
  sourceRangeCapacity: p.sourceRangeCapacity,
1840
1795
  prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1841
1796
  eventDensity: p.eventDensity,
@@ -2709,7 +2664,9 @@ let make = (
2709
2664
  // contract has no address-dependent events, so a later registration of the
2710
2665
  // same address still conflicts and the address is still persisted.
2711
2666
  addressStore
2712
- ->AddressStore.registerBatch(
2667
+ // These come from the config or from a resume, so they're already stored and
2668
+ // must never be drained back into a write.
2669
+ ->AddressStore.seedBatch(
2713
2670
  addresses->Array.map((contract): AddressStore.registration => {
2714
2671
  address: contract.address,
2715
2672
  contractName: contract.contractName,
@@ -1086,7 +1086,6 @@ function warnRejectedRegistration(verdict, chainId, contractAddress, contractNam
1086
1086
  }
1087
1087
  switch (verdict.TAG) {
1088
1088
  case "Added" :
1089
- case "NoEvents" :
1090
1089
  return;
1091
1090
  case "Duplicate" :
1092
1091
  let existingEffectiveStartBlock = verdict.existingEffectiveStartBlock;
@@ -1225,71 +1224,25 @@ function createPartitions(registeringSetsByContract, addressStore, dynamicContra
1225
1224
  return make(allPartitions, maxAddrInPartition, nextPartitionIndexRef.contents, dynamicContracts, clientFilteredContracts);
1226
1225
  }
1227
1226
 
1228
- function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, items) {
1227
+ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, registrations) {
1229
1228
  let claimCeiling = claimCeilingOpt !== undefined ? claimCeilingOpt : fetchState.knownHeight;
1230
1229
  if (Utils.$$Array.isEmpty(fetchState.normalSelection.onEventRegistrations)) {
1231
1230
  Stdlib_JsError.throwWithMessage("Invalid configuration. No events to fetch for the dynamic contract registration.");
1232
1231
  }
1233
- let registrations = [];
1234
- let sourceDcs = [];
1235
- let sourceIndexes = [];
1236
- for (let itemIdx = 0, itemIdx_finish = items.length; itemIdx < itemIdx_finish; ++itemIdx) {
1237
- let dcs = items[itemIdx].dcs;
1238
- if (dcs !== undefined) {
1239
- for (let dcIdx = 0, dcIdx_finish = dcs.length; dcIdx < dcIdx_finish; ++dcIdx) {
1240
- let dc = dcs[dcIdx];
1241
- registrations.push({
1242
- address: dc.address,
1243
- contractName: dc.contractName,
1244
- registrationBlock: dc.registrationBlock
1245
- });
1246
- sourceDcs.push(dcs);
1247
- sourceIndexes.push(dcIdx);
1248
- }
1249
- }
1250
- }
1251
1232
  let idCursor = addressStore.nextId();
1252
1233
  let verdicts = AddressStore.registerBatch(addressStore, registrations);
1253
1234
  let registeringContractNames = [];
1254
- let hasNoEventsUpdatesRef = false;
1255
1235
  for (let idx = 0, idx_finish = verdicts.length; idx < idx_finish; ++idx) {
1256
1236
  let registration = registrations[idx];
1257
1237
  let verdict = verdicts[idx];
1258
- let keep;
1259
1238
  let exit = 0;
1260
- if (typeof verdict !== "object") {
1239
+ if (typeof verdict !== "object" || verdict.TAG !== "Added") {
1261
1240
  exit = 1;
1262
- } else {
1263
- switch (verdict.TAG) {
1264
- case "Added" :
1265
- if (!registeringContractNames.includes(registration.contractName)) {
1266
- registeringContractNames.push(registration.contractName);
1267
- }
1268
- keep = true;
1269
- break;
1270
- case "NoEvents" :
1271
- hasNoEventsUpdatesRef = true;
1272
- warnAddressRegistration(fetchState.chainId, registration.address, {
1273
- contractName: registration.contractName
1274
- }, `Persisting contract registration without fetching: Contract doesn't have any events to fetch. It'll be picked up on restart if you add events for the contract.`);
1275
- keep = true;
1276
- break;
1277
- default:
1278
- exit = 1;
1279
- }
1241
+ } else if (verdict.fetchable && !registeringContractNames.includes(registration.contractName)) {
1242
+ registeringContractNames.push(registration.contractName);
1280
1243
  }
1281
1244
  if (exit === 1) {
1282
1245
  warnRejectedRegistration(verdict, fetchState.chainId, registration.address, registration.contractName);
1283
- keep = false;
1284
- }
1285
- if (!keep) {
1286
- sourceIndexes[idx] = -1 - sourceIndexes[idx] | 0;
1287
- }
1288
- }
1289
- for (let idx$1 = verdicts.length - 1 | 0; idx$1 >= 0; --idx$1) {
1290
- let marked = sourceIndexes[idx$1];
1291
- if (marked < 0) {
1292
- sourceDcs[idx$1].splice(-1 - marked | 0, 1);
1293
1246
  }
1294
1247
  }
1295
1248
  if (registeringContractNames.length === 0) {
@@ -1298,12 +1251,12 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1298
1251
  let newPartitions = [];
1299
1252
  let dynamicContractsRef = fetchState.optimizedPartitions.dynamicContracts;
1300
1253
  let mutExistingPartitions = Object.values(fetchState.optimizedPartitions.entities);
1301
- for (let idx$2 = 0, idx_finish$1 = registeringContractNames.length; idx$2 < idx_finish$1; ++idx$2) {
1302
- let contractName = registeringContractNames[idx$2];
1254
+ for (let idx$1 = 0, idx_finish$1 = registeringContractNames.length; idx$1 < idx_finish$1; ++idx$1) {
1255
+ let contractName = registeringContractNames[idx$1];
1303
1256
  if (!dynamicContractsRef.has(contractName)) {
1304
1257
  dynamicContractsRef = Utils.$$Set.immutableAdd(dynamicContractsRef, contractName);
1305
- for (let idx$3 = 0, idx_finish$2 = mutExistingPartitions.length; idx$3 < idx_finish$2; ++idx$3) {
1306
- let p = mutExistingPartitions[idx$3];
1258
+ for (let idx$2 = 0, idx_finish$2 = mutExistingPartitions.length; idx$2 < idx_finish$2; ++idx$2) {
1259
+ let p = mutExistingPartitions[idx$2];
1307
1260
  let match = p.addresses.countFor(contractName);
1308
1261
  if (match !== 0 && p.selection.dependsOnAddresses && p.mergeBlock === undefined) {
1309
1262
  let allPartitionContractNames = p.addresses.contractNames();
@@ -1312,7 +1265,7 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1312
1265
  if (!isFetching) {
1313
1266
  let newPartitionId = (fetchState.optimizedPartitions.nextPartitionIndex + newPartitions.length | 0).toString();
1314
1267
  let restNames = allPartitionContractNames.filter(name => name !== contractName);
1315
- mutExistingPartitions[idx$3] = withAddresses(p, p.addresses.filterByContracts(restNames));
1268
+ mutExistingPartitions[idx$2] = withAddresses(p, p.addresses.filterByContracts(restNames));
1316
1269
  let splitAddresses = p.addresses.filterByContracts([contractName]);
1317
1270
  newPartitions.push({
1318
1271
  id: newPartitionId,
@@ -1321,7 +1274,7 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1321
1274
  addresses: splitAddresses,
1322
1275
  mergeBlock: undefined,
1323
1276
  dynamicContract: contractName,
1324
- mutPendingQueries: p.mutPendingQueries,
1277
+ mutPendingQueries: [],
1325
1278
  sourceRangeCapacity: p.sourceRangeCapacity,
1326
1279
  prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1327
1280
  eventDensity: p.eventDensity,
@@ -1329,7 +1282,7 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1329
1282
  });
1330
1283
  }
1331
1284
  } else {
1332
- mutExistingPartitions[idx$3] = {
1285
+ mutExistingPartitions[idx$2] = {
1333
1286
  id: p.id,
1334
1287
  latestFetchedBlock: p.latestFetchedBlock,
1335
1288
  selection: p.selection,
@@ -1786,7 +1739,7 @@ function make$1(startBlock, endBlock, onEventRegistrations, addressStore, addres
1786
1739
  });
1787
1740
  }
1788
1741
  let normalSelection = makeSelection(normalRegistrations, true, undefined);
1789
- AddressStore.registerBatch(addressStore, addresses.map(contract => ({
1742
+ AddressStore.seedBatch(addressStore, addresses.map(contract => ({
1790
1743
  address: contract.address,
1791
1744
  contractName: contract.contractName,
1792
1745
  registrationBlock: contract.registrationBlock
@@ -157,46 +157,58 @@ let prepareRollbackDiff = async (
157
157
  }
158
158
  }
159
159
 
160
+ // Stages the addresses registered by this batch's events for the write. They're
161
+ // drained from each chain's address store rather than carried on the items that
162
+ // registered them: the store is where they already live, and it knows which
163
+ // ones the database hasn't seen yet.
160
164
  let setBatchDcs = (state: IndexerState.t, ~batch: Batch.t) => {
161
165
  let inMemTable = state->getInMemTable(~entityConfig=InternalTable.EnvioAddresses.entityConfig)
162
166
  let committedCheckpointId = state->IndexerState.committedCheckpointId
163
167
 
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
- )
168
+ batch.progressedChainsById->Utils.Dict.forEach(progressedChain => {
169
+ let chainId = progressedChain.fetchState.chainId
170
+ let chainState = state->IndexerState.getChainState(~chainId)
171
+
172
+ // Most batches register nothing, so the checkpoints are only collected for a
173
+ // chain that has something waiting.
174
+ if chainState->ChainState.hasAddressesToWrite {
175
+ // The store pairs each drained registration with a checkpoint of its own
176
+ // chain, so only this chain's checkpoints go in — and the index it returns
177
+ // points back into these two parallel arrays.
178
+ let checkpointIds = []
179
+ let checkpointBlockNumbers = []
180
+ for idx in 0 to batch.checkpointIds->Array.length - 1 {
181
+ if batch.checkpointChainIds->Array.getUnsafe(idx) === chainId {
182
+ checkpointIds->Array.push(batch.checkpointIds->Array.getUnsafe(idx))
183
+ checkpointBlockNumbers->Array.push(batch.checkpointBlockNumbers->Array.getUnsafe(idx))
196
184
  }
197
185
  }
198
- }
199
186
 
200
- itemIdx := itemIdx.contents + checkpointEventsProcessed
201
- }
187
+ chainState
188
+ ->ChainState.drainAddressesForWrite(
189
+ ~toBlockInclusive=progressedChain.progressBlockNumber,
190
+ ~checkpointBlockNumbers,
191
+ )
192
+ ->Array.forEach(dc => {
193
+ let entity: InternalTable.EnvioAddresses.t = {
194
+ id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address),
195
+ chainId,
196
+ contractName: dc.contractName,
197
+ registrationBlock: dc.registrationBlock,
198
+ // Only ever written, never read back. Kept on the table so the column
199
+ // doesn't need a migration.
200
+ registrationLogIndex: -1,
201
+ }
202
+
203
+ inMemTable->InMemoryTable.Entity.set(
204
+ ~committedCheckpointId,
205
+ Set({
206
+ entityId: entity.id->EntityId.unsafeOfString,
207
+ checkpointId: checkpointIds->Array.getUnsafe(dc.checkpointIdx),
208
+ entity: entity->InternalTable.EnvioAddresses.castToInternal,
209
+ }),
210
+ )
211
+ })
212
+ }
213
+ })
202
214
  }
@@ -4,6 +4,7 @@ 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";
8
9
  import * as IndexerState from "./IndexerState.res.mjs";
9
10
  import * as InMemoryTable from "./InMemoryTable.res.mjs";
@@ -117,39 +118,39 @@ async function prepareRollbackDiff(state, rollbackTargetCheckpointId, rollbackDi
117
118
  function setBatchDcs(state, batch) {
118
119
  let inMemTable = getInMemTable(state, Config.EnvioAddresses.entityConfig);
119
120
  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
- }
121
+ Utils.Dict.forEach(batch.progressedChainsById, progressedChain => {
122
+ let chainId = progressedChain.fetchState.chainId;
123
+ let chainState = IndexerState.getChainState(state, chainId);
124
+ if (!ChainState.hasAddressesToWrite(chainState)) {
125
+ return;
126
+ }
127
+ let checkpointIds = [];
128
+ let checkpointBlockNumbers = [];
129
+ for (let idx = 0, idx_finish = batch.checkpointIds.length; idx < idx_finish; ++idx) {
130
+ if (batch.checkpointChainIds[idx] === chainId) {
131
+ checkpointIds.push(batch.checkpointIds[idx]);
132
+ checkpointBlockNumbers.push(batch.checkpointBlockNumbers[idx]);
149
133
  }
150
134
  }
151
- itemIdx = itemIdx + checkpointEventsProcessed | 0;
152
- }
135
+ ChainState.drainAddressesForWrite(chainState, progressedChain.progressBlockNumber, checkpointBlockNumbers).forEach(dc => {
136
+ let entity_id = Config.EnvioAddresses.makeId(chainId, dc.address);
137
+ let entity_registration_block = dc.registrationBlock;
138
+ let entity_contract_name = dc.contractName;
139
+ let entity = {
140
+ id: entity_id,
141
+ chain_id: chainId,
142
+ registration_block: entity_registration_block,
143
+ registration_log_index: -1,
144
+ contract_name: entity_contract_name
145
+ };
146
+ InMemoryTable.Entity.set(inMemTable, committedCheckpointId, {
147
+ type: "SET",
148
+ entityId: entity_id,
149
+ entity: entity,
150
+ checkpointId: checkpointIds[dc.checkpointIdx]
151
+ });
152
+ });
153
+ });
153
154
  }
154
155
 
155
156
  export {
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,
@@ -119,9 +119,6 @@ module Async = {
119
119
  @module("vitest") @scope("it")
120
120
  external it_fails: (string, testContext => promise<unit>) => unit = "fails"
121
121
 
122
- let isClaudeCloud: bool = %raw(`process.env.CLAUDE_CODE_CONTAINER_ID != null`)
123
- let itSkipInClaudeCloud = (name, fn) => it_skipIf(isClaudeCloud)(name, fn)
124
-
125
122
  @module("vitest")
126
123
  external test: (string, testContext => promise<unit>) => unit = "test"
127
124
 
@@ -1,17 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Vitest from "vitest";
4
3
 
5
- let isClaudeCloud = (process.env.CLAUDE_CODE_CONTAINER_ID != null);
6
-
7
- function itSkipInClaudeCloud(name, fn) {
8
- Vitest.it.skipIf(isClaudeCloud)(name, fn);
9
- }
10
-
11
- let Async = {
12
- isClaudeCloud: isClaudeCloud,
13
- itSkipInClaudeCloud: itSkipInClaudeCloud
14
- };
4
+ let Async = {};
15
5
 
16
6
  let messageOfThrown = (function (fn) {
17
7
  try {
@@ -31,4 +21,4 @@ export {
31
21
  messageOfThrown,
32
22
  toThrowErrorEqual,
33
23
  }
34
- /* isClaudeCloud Not a pure module */
24
+ /* No side effect */
@@ -7,10 +7,12 @@
7
7
  // (or address-valued param) isn't registered at the item's block.
8
8
  type t
9
9
 
10
- // A contract the chain has events for. An address registered for anything else
11
- // gets the `NoEvents` verdict: persisted so a future config that adds events
12
- // picks it up, but never fetched.
13
- type contract = {name: string, startBlock: option<int>}
10
+ // A contract an address may be registered for. Every config contract of every
11
+ // chain, since `context.chain.<Contract>.add` validates against that whole set
12
+ // a name outside it makes the store throw. `dependsOnAddresses` is whether
13
+ // this chain fetches for the contract by address, which is what makes the store
14
+ // able to answer `fetchable` on a verdict.
15
+ type contract = {name: string, startBlock: option<int>, dependsOnAddresses: bool}
14
16
 
15
17
  type registration = {
16
18
  address: Address.t,
@@ -20,9 +22,10 @@ type registration = {
20
22
  }
21
23
 
22
24
  type verdict =
23
- | Added({effectiveStartBlock: int})
24
- // Contract has no events; tracked and persisted, never fetched.
25
- | NoEvents({effectiveStartBlock: int})
25
+ // `fetchable` is false when nothing on the chain is fetched by address for the
26
+ // contract: the address is still stored and persisted, so a config that later
27
+ // adds events picks it up on restart, but no partition is built from it.
28
+ | Added({effectiveStartBlock: int, fetchable: bool})
26
29
  // Already registered for the same contract.
27
30
  | Duplicate({effectiveStartBlock: int, existingEffectiveStartBlock: int})
28
31
  // Already registered for a different contract.
@@ -32,11 +35,22 @@ type verdict =
32
35
 
33
36
  type rawVerdict = {
34
37
  kind: string,
38
+ fetchable: bool,
35
39
  effectiveStartBlock: int,
36
40
  existingContractName: Null.t<string>,
37
41
  existingEffectiveStartBlock: Null.t<int>,
38
42
  }
39
43
 
44
+ // A registration the store handed over for persistence, paired with the
45
+ // checkpoint that owns its row. `checkpointIdx` indexes the block numbers
46
+ // passed to `drainForWrite` — the ids stay on the JS side.
47
+ type drainedAddress = {
48
+ address: Address.t,
49
+ contractName: string,
50
+ registrationBlock: int,
51
+ checkpointIdx: int,
52
+ }
53
+
40
54
  type makeSetOptions = {
41
55
  // Only addresses whose id is at or above this. Ids are handed out in
42
56
  // registration order, so a cursor read before a batch (`nextId`) selects
@@ -66,22 +80,37 @@ let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool, ~contracts: array
66
80
  }
67
81
  }
68
82
 
69
- // The contracts of a chain that have events, with the earliest block any of
70
- // their events may fire at. `None` means unrestricted, so it wins over any
71
- // `Some`: one registration without a start block makes the whole contract's
72
- // address gate open from the chain's start, and the per-registration start
73
- // block still holds back the registrations that declared one.
74
- let contractsOf = (~onEventRegistrations: array<Internal.onEventRegistration>): array<contract> => {
83
+ // Every contract an address may be registered for, with the earliest block any
84
+ // of this chain's events for it may fire at. `None` means unrestricted, so it
85
+ // wins over any `Some`: one registration without a start block makes the whole
86
+ // contract's address gate open from the chain's start, and the per-registration
87
+ // start block still holds back the registrations that declared one.
88
+ //
89
+ // A contract whose events are all wildcard is fetched without consulting
90
+ // addresses, so registering one changes nothing about what's queried — it
91
+ // carries `dependsOnAddresses: false` just like a contract named only by
92
+ // `configContractNames`. Either way the addresses are stored and persisted,
93
+ // never fetched.
94
+ let contractsOf = (
95
+ ~onEventRegistrations: array<Internal.onEventRegistration>,
96
+ ~configContractNames: array<string>,
97
+ ): array<contract> => {
75
98
  // Only ever holds a declared start block, so a missing key is unambiguous —
76
99
  // storing `None` in a dict would be indistinguishable from "not seen yet".
77
100
  let startBlocks: dict<int> = Dict.make()
78
101
  let unrestricted = Utils.Set.make()
102
+ let addressDependent = Utils.Set.make()
79
103
  let names = []
80
- onEventRegistrations->Array.forEach(reg => {
81
- let name = reg.eventConfig.contractName
104
+ let addName = name =>
82
105
  if !(names->Array.includes(name)) {
83
106
  names->Array.push(name)->ignore
84
107
  }
108
+ onEventRegistrations->Array.forEach(reg => {
109
+ let name = reg.eventConfig.contractName
110
+ addName(name)
111
+ if reg.dependsOnAddresses {
112
+ addressDependent->Utils.Set.add(name)->ignore
113
+ }
85
114
  switch reg.startBlock {
86
115
  | None => unrestricted->Utils.Set.add(name)->ignore
87
116
  | Some(startBlock) =>
@@ -94,11 +123,13 @@ let contractsOf = (~onEventRegistrations: array<Internal.onEventRegistration>):
94
123
  )
95
124
  }
96
125
  })
126
+ configContractNames->Array.forEach(addName)
97
127
  names->Array.map(name => {
98
128
  name,
99
129
  startBlock: unrestricted->Utils.Set.has(name)
100
130
  ? None
101
131
  : startBlocks->Utils.Dict.dangerouslyGetNonOption(name),
132
+ dependsOnAddresses: addressDependent->Utils.Set.has(name),
102
133
  })
103
134
  }
104
135
 
@@ -112,10 +143,24 @@ let contractsOf = (~onEventRegistrations: array<Internal.onEventRegistration>):
112
143
  // come from that chain's one store — ids are store-scoped, so sets from
113
144
  // different stores can't be merged.
114
145
  @send external makeSetOf: (t, array<Address.t>) => AddressSet.t = "makeSetOf"
115
- @send external registerBatchRaw: (t, array<registration>) => array<rawVerdict> = "registerBatch"
116
- @send external makeSetRaw: (t, string, makeSetOptions) => AddressSet.t = "makeSet"
117
146
  @send
118
- external startBlockGroups: (t, string) => array<AddressSet.startBlockGroup> = "startBlockGroups"
147
+ external registerBatchRaw: (t, array<registration>) => array<rawVerdict> = "registerBatch"
148
+ @send external seedBatchRaw: (t, array<registration>) => array<rawVerdict> = "seedBatch"
149
+
150
+ // Drains the registrations awaiting persistence at or below the given block —
151
+ // what the batch being written covers — pairing each with the checkpoint at its
152
+ // registration block. Later registrations stay pending. Throws, with the queue
153
+ // untouched, when a drained registration's block has no checkpoint in the batch.
154
+ @send
155
+ external drainForWrite: (t, int, array<int>) => array<drainedAddress> = "drainForWrite"
156
+
157
+ // How many registrations await persistence — lets a caller skip the work of
158
+ // assembling what `drainForWrite` needs.
159
+ @send external pendingCount: t => int = "pendingCount"
160
+
161
+ // The registrations still awaiting persistence. For assertions.
162
+ @send external pendingEntries: t => array<Internal.indexingContract> = "pendingEntries"
163
+ @send external makeSetRaw: (t, string, makeSetOptions) => AddressSet.t = "makeSet"
119
164
  @send external contractCount: (t, string) => int = "contractCount"
120
165
  @send external size: t => int = "size"
121
166
 
@@ -132,12 +177,9 @@ external startBlockGroups: (t, string) => array<AddressSet.startBlockGroup> = "s
132
177
 
133
178
  @send external contractAddresses: (t, string) => array<Address.t> = "contractAddresses"
134
179
 
135
- @send external entries: t => array<Internal.indexingContract> = "entries"
136
-
137
180
  let toVerdict = (raw: rawVerdict): verdict =>
138
181
  switch raw.kind {
139
- | "added" => Added({effectiveStartBlock: raw.effectiveStartBlock})
140
- | "noEvents" => NoEvents({effectiveStartBlock: raw.effectiveStartBlock})
182
+ | "added" => Added({effectiveStartBlock: raw.effectiveStartBlock, fetchable: raw.fetchable})
141
183
  | "duplicate" =>
142
184
  Duplicate({
143
185
  effectiveStartBlock: raw.effectiveStartBlock,
@@ -148,13 +190,22 @@ let toVerdict = (raw: rawVerdict): verdict =>
148
190
  | kind => JsError.throwWithMessage(`Unexpected address registration verdict "${kind}"`)
149
191
  }
150
192
 
151
- // Registers a batch, resolving each address against both the store and the
152
- // batch's own earlier entries — so two contracts claiming one address inside a
153
- // single batch conflict just as they would across batches. Verdicts come back
154
- // in the batch's order.
193
+ // Registers dynamic registrations, resolving each address against both the
194
+ // store and the batch's own earlier entries — so two contracts claiming one
195
+ // address inside a single batch conflict just as they would across batches.
196
+ // Verdicts come back in the batch's order. What it adds is pending persistence
197
+ // until a batch write drains it.
198
+ //
199
+ // An unknown contract name throws with none of the batch applied.
155
200
  let registerBatch = (store: t, registrations: array<registration>): array<verdict> =>
156
201
  store->registerBatchRaw(registrations)->Array.map(toVerdict)
157
202
 
203
+ // `registerBatch` for addresses the database already holds — config addresses
204
+ // and the dynamic ones a resume restores. Nothing is marked pending, so nothing
205
+ // is ever written back.
206
+ let seedBatch = (store: t, registrations: array<registration>): array<verdict> =>
207
+ store->seedBatchRaw(registrations)->Array.map(toVerdict)
208
+
158
209
  let makeSet = (store: t, ~contractName, ~options={}: makeSetOptions) =>
159
210
  store->makeSetRaw(contractName, options)
160
211
 
@@ -17,14 +17,22 @@ function make(ecosystem, shouldChecksum, contracts) {
17
17
  }
18
18
  }
19
19
 
20
- function contractsOf(onEventRegistrations) {
20
+ function contractsOf(onEventRegistrations, configContractNames) {
21
21
  let startBlocks = {};
22
22
  let unrestricted = new Set();
23
+ let addressDependent = new Set();
23
24
  let names = [];
24
- onEventRegistrations.forEach(reg => {
25
- let name = reg.eventConfig.contractName;
25
+ let addName = name => {
26
26
  if (!names.includes(name)) {
27
27
  names.push(name);
28
+ return;
29
+ }
30
+ };
31
+ onEventRegistrations.forEach(reg => {
32
+ let name = reg.eventConfig.contractName;
33
+ addName(name);
34
+ if (reg.dependsOnAddresses) {
35
+ addressDependent.add(name);
28
36
  }
29
37
  let startBlock = reg.startBlock;
30
38
  if (startBlock !== undefined) {
@@ -34,9 +42,11 @@ function contractsOf(onEventRegistrations) {
34
42
  }
35
43
  unrestricted.add(name);
36
44
  });
45
+ configContractNames.forEach(addName);
37
46
  return names.map(name => ({
38
47
  name: name,
39
- startBlock: unrestricted.has(name) ? undefined : startBlocks[name]
48
+ startBlock: unrestricted.has(name) ? undefined : startBlocks[name],
49
+ dependsOnAddresses: addressDependent.has(name)
40
50
  }));
41
51
  }
42
52
 
@@ -46,7 +56,8 @@ function toVerdict(raw) {
46
56
  case "added" :
47
57
  return {
48
58
  TAG: "Added",
49
- effectiveStartBlock: raw.effectiveStartBlock
59
+ effectiveStartBlock: raw.effectiveStartBlock,
60
+ fetchable: raw.fetchable
50
61
  };
51
62
  case "conflict" :
52
63
  return {
@@ -61,11 +72,6 @@ function toVerdict(raw) {
61
72
  };
62
73
  case "invalid" :
63
74
  return "Invalid";
64
- case "noEvents" :
65
- return {
66
- TAG: "NoEvents",
67
- effectiveStartBlock: raw.effectiveStartBlock
68
- };
69
75
  default:
70
76
  return Stdlib_JsError.throwWithMessage(`Unexpected address registration verdict "` + kind + `"`);
71
77
  }
@@ -75,6 +81,10 @@ function registerBatch(store, registrations) {
75
81
  return store.registerBatch(registrations).map(toVerdict);
76
82
  }
77
83
 
84
+ function seedBatch(store, registrations) {
85
+ return store.seedBatch(registrations).map(toVerdict);
86
+ }
87
+
78
88
  function makeSet(store, contractName, optionsOpt) {
79
89
  let options = optionsOpt !== undefined ? optionsOpt : ({});
80
90
  return store.makeSet(contractName, options);
@@ -89,6 +99,7 @@ export {
89
99
  contractsOf,
90
100
  toVerdict,
91
101
  registerBatch,
102
+ seedBatch,
92
103
  makeSet,
93
104
  get,
94
105
  }