envio 3.5.0-alpha.2 → 3.5.0-alpha.3

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-alpha.2",
3
+ "version": "3.5.0-alpha.3",
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-alpha.2",
73
- "envio-linux-x64-musl": "3.5.0-alpha.2",
74
- "envio-linux-arm64": "3.5.0-alpha.2",
75
- "envio-darwin-x64": "3.5.0-alpha.2",
76
- "envio-darwin-arm64": "3.5.0-alpha.2"
72
+ "envio-linux-x64": "3.5.0-alpha.3",
73
+ "envio-linux-x64-musl": "3.5.0-alpha.3",
74
+ "envio-linux-arm64": "3.5.0-alpha.3",
75
+ "envio-darwin-x64": "3.5.0-alpha.3",
76
+ "envio-darwin-arm64": "3.5.0-alpha.3"
77
77
  }
78
78
  }
@@ -104,6 +104,13 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
104
104
  // When resuming from persisted state, all events may already be processed.
105
105
  if EventProcessing.allChainsEventsProcessedToEndblock(state->IndexerState.chainStates) {
106
106
  Logging.info("All chains are caught up to end blocks.")
107
+ // A run that has nothing left to process still owes the schema its
108
+ // deferred indexes — otherwise a resume of a backfill that died right
109
+ // before finalizing would report ready without them.
110
+ state->IndexerState.markCaughtUp
111
+ if state->IndexerState.isFinalizingIndexes {
112
+ await FinalizeBackfill.run(state)
113
+ }
107
114
  if !(state->IndexerState.keepProcessAlive) {
108
115
  await ExitOnCaughtUp.run(state)
109
116
  }
@@ -156,6 +163,13 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
156
163
  state->IndexerState.clearRollback
157
164
  state->IndexerState.applyBatchProgress(~batch)
158
165
 
166
+ // Backfilling → FinalizingIndexes → Ready. Awaiting here holds the
167
+ // processing loop for the whole finalize, which is what pauses
168
+ // processing while the indexes are built.
169
+ if state->IndexerState.isFinalizingIndexes {
170
+ await FinalizeBackfill.run(state)
171
+ }
172
+
159
173
  if !isRealtimeBeforeUpdate && state->IndexerState.isRealtime {
160
174
  // Catching up just flipped the chain to realtime, which changes the
161
175
  // active source for waitForNewBlock (eg sync -> live). The waiter that
@@ -9,6 +9,7 @@ import * as ErrorHandling from "./ErrorHandling.res.mjs";
9
9
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
10
10
  import * as ExitOnCaughtUp from "./ExitOnCaughtUp.res.mjs";
11
11
  import * as EventProcessing from "./EventProcessing.res.mjs";
12
+ import * as FinalizeBackfill from "./FinalizeBackfill.res.mjs";
12
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
13
14
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
14
15
  import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
@@ -38,6 +39,10 @@ async function processNextBatch(state, scheduleFetch) {
38
39
  }
39
40
  if (EventProcessing.allChainsEventsProcessedToEndblock(IndexerState.chainStates(state))) {
40
41
  Logging.info("All chains are caught up to end blocks.");
42
+ IndexerState.markCaughtUp(state);
43
+ if (IndexerState.isFinalizingIndexes(state)) {
44
+ await FinalizeBackfill.run(state);
45
+ }
41
46
  if (!IndexerState.keepProcessAlive(state)) {
42
47
  return await ExitOnCaughtUp.run(state);
43
48
  } else {
@@ -64,6 +69,9 @@ async function processNextBatch(state, scheduleFetch) {
64
69
  }
65
70
  IndexerState.clearRollback(state);
66
71
  IndexerState.applyBatchProgress(state, batch);
72
+ if (IndexerState.isFinalizingIndexes(state)) {
73
+ await FinalizeBackfill.run(state);
74
+ }
67
75
  if (!isRealtimeBeforeUpdate && IndexerState.isRealtime(state)) {
68
76
  IndexerState.invalidateInflight(state);
69
77
  scheduleFetch();
@@ -147,6 +147,7 @@ let makeInternal = (
147
147
  ~reorgCheckpoints: array<Internal.reorgCheckpoint>,
148
148
  ~maxReorgDepth,
149
149
  ~knownHeight=0,
150
+ ~isResumed=false,
150
151
  ~reducedPollingInterval=?,
151
152
  ): t => {
152
153
  // Handler binding + `where`-derived fetch state, and onBlock registrations,
@@ -203,6 +204,7 @@ let makeInternal = (
203
204
  | Evm => Some(config.clientFilterAddressThreshold)
204
205
  | Fuel | Svm => None
205
206
  },
207
+ ~isResumed,
206
208
  )
207
209
 
208
210
  let chainReorgCheckpoints = reorgCheckpoints->Array.filterMap(reorgCheckpoint => {
@@ -383,6 +385,7 @@ let makeFromDbState = (
383
385
  ~isInReorgThreshold,
384
386
  ~isRealtime,
385
387
  ~knownHeight=resumedChainState.sourceBlockNumber,
388
+ ~isResumed=true,
386
389
  ~reducedPollingInterval?,
387
390
  )
388
391
  }
@@ -1082,11 +1085,14 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t, ~blockTimestampName: string) =
1082
1085
  }
1083
1086
 
1084
1087
  // Mark the chain caught up to head/endblock. Called by CrossChainState only once
1085
- // every chain in the indexer is caught up, so no chain flips to ready while
1086
- // another is still backfilling. Sticky: a chain stays ready once set.
1087
- let markReady = (cs: t) =>
1088
+ // every chain in the indexer is caught up and the deferred schema indexes are
1089
+ // committed, so no chain flips to ready while another is still backfilling or
1090
+ // while an index the schema promises is still missing. `readyAt` is the
1091
+ // timestamp already committed to `envio_chains.ready_at` in that same
1092
+ // transaction. Sticky: a chain stays ready once set.
1093
+ let markReady = (cs: t, ~readyAt) =>
1088
1094
  if !(cs->isReady) {
1089
- cs.timestampCaughtUpToHeadOrEndblock = Date.make()->Some
1095
+ cs.timestampCaughtUpToHeadOrEndblock = Some(readyAt)
1090
1096
  }
1091
1097
 
1092
1098
  // Roll a chain back to a reorg target. With a progress diff, restore fetch/
@@ -89,9 +89,10 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
89
89
  };
90
90
  }
91
91
 
92
- function makeInternal(chainConfig, indexingAddresses, startBlock, endBlock, firstEventBlockOpt, progressBlockNumber, config, registrationsByChainId, logger, timestampCaughtUpToHeadOrEndblock, numEventsProcessed, isInReorgThreshold, isRealtime, reorgCheckpoints, maxReorgDepth, knownHeightOpt, reducedPollingInterval) {
92
+ function makeInternal(chainConfig, indexingAddresses, startBlock, endBlock, firstEventBlockOpt, progressBlockNumber, config, registrationsByChainId, logger, timestampCaughtUpToHeadOrEndblock, numEventsProcessed, isInReorgThreshold, isRealtime, reorgCheckpoints, maxReorgDepth, knownHeightOpt, isResumedOpt, reducedPollingInterval) {
93
93
  let firstEventBlock = firstEventBlockOpt !== undefined ? Primitive_option.valFromOption(firstEventBlockOpt) : undefined;
94
94
  let knownHeight = knownHeightOpt !== undefined ? knownHeightOpt : 0;
95
+ let isResumed = isResumedOpt !== undefined ? isResumedOpt : false;
95
96
  let match = Stdlib_Option.getOr(registrationsByChainId[chainConfig.id.toString()], {
96
97
  onEventRegistrations: [],
97
98
  onBlockRegistrations: []
@@ -116,7 +117,7 @@ function makeInternal(chainConfig, indexingAddresses, startBlock, endBlock, firs
116
117
  tmp = undefined;
117
118
  break;
118
119
  }
119
- let fetchState = FetchState.make(startBlock, endBlock, onEventRegistrations, addressStore, indexingAddresses, config.maxAddrInPartition, chainConfig.id, (config.batchSize << 1), knownHeight, progressBlockNumber, match.onBlockRegistrations, Primitive_int.max(!config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth, chainConfig.blockLag), Primitive_option.some(firstEventBlock), Primitive_option.some(tmp));
120
+ let fetchState = FetchState.make(startBlock, endBlock, onEventRegistrations, addressStore, indexingAddresses, config.maxAddrInPartition, chainConfig.id, (config.batchSize << 1), knownHeight, progressBlockNumber, match.onBlockRegistrations, Primitive_int.max(!config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth, chainConfig.blockLag), Primitive_option.some(firstEventBlock), Primitive_option.some(tmp), isResumed);
120
121
  let chainReorgCheckpoints = Stdlib_Array.filterMap(reorgCheckpoints, reorgCheckpoint => {
121
122
  if (reorgCheckpoint.chain_id === chainConfig.id) {
122
123
  return reorgCheckpoint;
@@ -177,7 +178,7 @@ function makeFromConfig(chainConfig, config, registrationsByChainId, knownHeight
177
178
  let logger = Logging.createChild({
178
179
  chainId: chainConfig.id
179
180
  });
180
- return makeInternal(chainConfig, configAddresses(chainConfig), chainConfig.startBlock, chainConfig.endBlock, undefined, -1, config, registrationsByChainId, logger, undefined, 0, false, false, [], chainConfig.maxReorgDepth, knownHeight, undefined);
181
+ return makeInternal(chainConfig, configAddresses(chainConfig), chainConfig.startBlock, chainConfig.endBlock, undefined, -1, config, registrationsByChainId, logger, undefined, 0, false, false, [], chainConfig.maxReorgDepth, knownHeight, undefined, undefined);
181
182
  }
182
183
 
183
184
  function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrationsByChainId, reducedPollingInterval) {
@@ -186,7 +187,7 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
186
187
  chainId: chainId
187
188
  });
188
189
  let progressBlockNumber = resumedChainState.progressBlockNumber >= 0 ? resumedChainState.progressBlockNumber : resumedChainState.startBlock - 1 | 0;
189
- return makeInternal(chainConfig, resumedChainState.indexingAddresses, resumedChainState.startBlock, resumedChainState.endBlock, Primitive_option.some(resumedChainState.firstEventBlockNumber), progressBlockNumber, config, registrationsByChainId, logger, Env.updateSyncTimeOnRestart ? undefined : resumedChainState.timestampCaughtUpToHeadOrEndblock, resumedChainState.numEventsProcessed, isInReorgThreshold, isRealtime, reorgCheckpoints, resumedChainState.maxReorgDepth, resumedChainState.sourceBlockNumber, reducedPollingInterval);
190
+ return makeInternal(chainConfig, resumedChainState.indexingAddresses, resumedChainState.startBlock, resumedChainState.endBlock, Primitive_option.some(resumedChainState.firstEventBlockNumber), progressBlockNumber, config, registrationsByChainId, logger, Env.updateSyncTimeOnRestart ? undefined : resumedChainState.timestampCaughtUpToHeadOrEndblock, resumedChainState.numEventsProcessed, isInReorgThreshold, isRealtime, reorgCheckpoints, resumedChainState.maxReorgDepth, resumedChainState.sourceBlockNumber, true, reducedPollingInterval);
190
191
  }
191
192
 
192
193
  function logger(cs) {
@@ -757,9 +758,9 @@ function applyBatchProgress(cs, batch, blockTimestampName) {
757
758
  }
758
759
  }
759
760
 
760
- function markReady(cs) {
761
+ function markReady(cs, readyAt) {
761
762
  if (cs.timestampCaughtUpToHeadOrEndblock === undefined) {
762
- cs.timestampCaughtUpToHeadOrEndblock = new Date();
763
+ cs.timestampCaughtUpToHeadOrEndblock = readyAt;
763
764
  return;
764
765
  }
765
766
  }
@@ -137,7 +137,7 @@ let setEndBlockToFirstEvent: (t, ~blockNumber: int) => unit
137
137
  let enterReorgThreshold: t => unit
138
138
  let advanceAfterBatch: (t, ~batch: Batch.t, ~enteringReorgThreshold: bool) => unit
139
139
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
140
- let markReady: t => unit
140
+ let markReady: (t, ~readyAt: Date.t) => unit
141
141
  let rollback: (
142
142
  t,
143
143
  ~newProgressBlockNumber: option<int>,
package/src/Config.res CHANGED
@@ -455,7 +455,7 @@ let parseEntitiesFromJson = (
455
455
  )
456
456
  )
457
457
 
458
- let compositeIndices =
458
+ let compositeIndexes =
459
459
  entityJson["compositeIndices"]
460
460
  ->Option.getOr([])
461
461
  ->Array.map(ci =>
@@ -470,7 +470,7 @@ let parseEntitiesFromJson = (
470
470
  let table = Table.mkTable(
471
471
  entityName,
472
472
  ~fields=Array.concat(fields, derivedFields),
473
- ~compositeIndices,
473
+ ~compositeIndexes,
474
474
  ~description=?entityJson["description"],
475
475
  )
476
476
 
@@ -331,11 +331,11 @@ function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage) {
331
331
  return Table.mkField(prop.name, match[0], match[1], undefined, match[3], match[2], prop.name === "id", match[4], prop.linkedEntity, prop.description, prop.postgresDbName, prop.clickhouseDbName);
332
332
  });
333
333
  let derivedFields = Stdlib_Option.getOr(entityJson.derivedFields, []).map(df => Table.mkDerivedFromField(df.fieldName, df.derivedFromEntity, df.derivedFromField, df.description));
334
- let compositeIndices = Stdlib_Option.getOr(entityJson.compositeIndices, []).map(ci => ci.map(f => ({
334
+ let compositeIndexes = Stdlib_Option.getOr(entityJson.compositeIndices, []).map(ci => ci.map(f => ({
335
335
  fieldName: f.fieldName,
336
336
  direction: f.direction === "Asc" ? "Asc" : "Desc"
337
337
  })));
338
- let table = Table.mkTable(entityName, compositeIndices, fields.concat(derivedFields), entityJson.description);
338
+ let table = Table.mkTable(entityName, compositeIndexes, fields.concat(derivedFields), entityJson.description);
339
339
  let getApiFieldName = prop => {
340
340
  let match = prop.linkedEntity;
341
341
  if (match !== undefined) {
@@ -10,6 +10,10 @@ type t = {
10
10
  chainIds: array<int>,
11
11
  // True once every chain has caught up to head/endBlock. Monotonic during a run.
12
12
  mutable isRealtime: bool,
13
+ // True once every chain has caught up and there's nothing left to process,
14
+ // but before the deferred schema indexes and `ready_at` are committed. The
15
+ // gap between this and `isRealtime` is the FinalizingIndexes phase.
16
+ mutable isCaughtUp: bool,
13
17
  mutable isInReorgThreshold: bool,
14
18
  // Indexer-wide fetch buffer pool (item count), shared across all chains.
15
19
  targetBufferSize: int,
@@ -32,6 +36,7 @@ let make = (
32
36
  chainStates,
33
37
  chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
34
38
  isRealtime,
39
+ isCaughtUp: isRealtime,
35
40
  isInReorgThreshold,
36
41
  targetBufferSize,
37
42
  }
@@ -46,6 +51,7 @@ let getChainState = (crossChainState: t, chainId) =>
46
51
 
47
52
  let chainStates = (crossChainState: t) => crossChainState.chainStates
48
53
  let isRealtime = (crossChainState: t) => crossChainState.isRealtime
54
+ let isCaughtUp = (crossChainState: t) => crossChainState.isCaughtUp
49
55
  let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
50
56
  let targetBufferSize = (crossChainState: t) => crossChainState.targetBufferSize
51
57
 
@@ -129,10 +135,11 @@ let enterReorgThreshold = (crossChainState: t) => {
129
135
  crossChainState.isInReorgThreshold = true
130
136
  }
131
137
 
132
- // Commit each progressed chain's batch progress, then decide readiness for the
133
- // whole indexer. A chain is marked caught up only once EVERY chain is caught up
134
- // (reached endblock or fetched/processed to head) with no processable events
135
- // left so no chain flips to ready while another is still backfilling.
138
+ // Commit each progressed chain's batch progress, then record whether the whole
139
+ // indexer has caught up (every chain reached endblock or processed to head,
140
+ // with no processable events left). Catching up doesn't make the indexer ready:
141
+ // the deferred schema indexes still have to be created, which `markReady`
142
+ // below concludes once they're committed.
136
143
  let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampName: string) => {
137
144
  let chainIds = crossChainState.chainIds
138
145
 
@@ -145,15 +152,14 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
145
152
  }
146
153
  }
147
154
 
148
- let indexerCaughtUp = crossChainState->nextItemIsNone && everyChainCaughtUp.contents
155
+ crossChainState.isCaughtUp =
156
+ crossChainState.isCaughtUp || (crossChainState->nextItemIsNone && everyChainCaughtUp.contents)
149
157
 
158
+ // A run resumed with every chain already stamped ready needs no finalize —
159
+ // the indexes were committed together with those stamps.
150
160
  let allChainsReady = ref(true)
151
161
  for i in 0 to chainIds->Array.length - 1 {
152
- let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
153
- if indexerCaughtUp {
154
- cs->ChainState.markReady
155
- }
156
- if !(cs->ChainState.isReady) {
162
+ if !(crossChainState->getChainState(chainIds->Array.getUnsafe(i))->ChainState.isReady) {
157
163
  allChainsReady := false
158
164
  }
159
165
  }
@@ -161,6 +167,23 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
161
167
  crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
162
168
  }
163
169
 
170
+ // Enter the FinalizingIndexes phase without a batch. Used when the loop finds
171
+ // every chain already processed to its endblock — a resume that has nothing
172
+ // left to do still owes the schema its deferred indexes before it reports ready.
173
+ let markCaughtUp = (crossChainState: t) => crossChainState.isCaughtUp = true
174
+
175
+ // Concludes the FinalizingIndexes phase: stamps every chain with the `ready_at`
176
+ // already committed alongside the deferred schema indexes and switches the
177
+ // indexer to realtime.
178
+ let markReady = (crossChainState: t, ~readyAt) => {
179
+ for i in 0 to crossChainState.chainIds->Array.length - 1 {
180
+ crossChainState
181
+ ->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
182
+ ->ChainState.markReady(~readyAt)
183
+ }
184
+ crossChainState.isRealtime = true
185
+ }
186
+
164
187
  // --- Fetch control. ---
165
188
 
166
189
  // Chains ordered furthest-behind first by fetch-frontier progress, so the
@@ -27,6 +27,7 @@ function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt)
27
27
  chainStates: chainStates,
28
28
  chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
29
29
  isRealtime: isRealtime,
30
+ isCaughtUp: isRealtime,
30
31
  isInReorgThreshold: isInReorgThreshold,
31
32
  targetBufferSize: targetBufferSize
32
33
  };
@@ -40,6 +41,10 @@ function isRealtime(crossChainState) {
40
41
  return crossChainState.isRealtime;
41
42
  }
42
43
 
44
+ function isCaughtUp(crossChainState) {
45
+ return crossChainState.isCaughtUp;
46
+ }
47
+
43
48
  function isInReorgThreshold(crossChainState) {
44
49
  return crossChainState.isInReorgThreshold;
45
50
  }
@@ -104,20 +109,27 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
104
109
  everyChainCaughtUp = false;
105
110
  }
106
111
  }
107
- let indexerCaughtUp = nextItemIsNone(crossChainState) && everyChainCaughtUp;
112
+ crossChainState.isCaughtUp = crossChainState.isCaughtUp || nextItemIsNone(crossChainState) && everyChainCaughtUp;
108
113
  let allChainsReady = true;
109
114
  for (let i$1 = 0, i_finish$1 = chainIds.length; i$1 < i_finish$1; ++i$1) {
110
- let cs$1 = crossChainState.chainStates[chainIds[i$1]];
111
- if (indexerCaughtUp) {
112
- ChainState.markReady(cs$1);
113
- }
114
- if (!ChainState.isReady(cs$1)) {
115
+ if (!ChainState.isReady(crossChainState.chainStates[chainIds[i$1]])) {
115
116
  allChainsReady = false;
116
117
  }
117
118
  }
118
119
  crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady;
119
120
  }
120
121
 
122
+ function markCaughtUp(crossChainState) {
123
+ crossChainState.isCaughtUp = true;
124
+ }
125
+
126
+ function markReady(crossChainState, readyAt) {
127
+ for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
128
+ ChainState.markReady(crossChainState.chainStates[crossChainState.chainIds[i]], readyAt);
129
+ }
130
+ crossChainState.isRealtime = true;
131
+ }
132
+
121
133
  function priorityOrder(crossChainState) {
122
134
  return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.frontierProgress(a), ChainState.frontierProgress(b)));
123
135
  }
@@ -222,6 +234,7 @@ export {
222
234
  make,
223
235
  chainStates,
224
236
  isRealtime,
237
+ isCaughtUp,
225
238
  isInReorgThreshold,
226
239
  targetBufferSize,
227
240
  nextItemIsNone,
@@ -229,6 +242,8 @@ export {
229
242
  createBatch,
230
243
  enterReorgThreshold,
231
244
  applyBatchProgress,
245
+ markCaughtUp,
246
+ markReady,
232
247
  priorityOrder,
233
248
  checkAndFetch,
234
249
  }
@@ -15,6 +15,7 @@ let make: (
15
15
  // Accessors.
16
16
  let chainStates: t => dict<ChainState.t>
17
17
  let isRealtime: t => bool
18
+ let isCaughtUp: t => bool
18
19
  let isInReorgThreshold: t => bool
19
20
  let targetBufferSize: t => int
20
21
 
@@ -31,6 +32,8 @@ let createBatch: (
31
32
  ) => Batch.t
32
33
  let enterReorgThreshold: t => unit
33
34
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
35
+ let markCaughtUp: t => unit
36
+ let markReady: (t, ~readyAt: Date.t) => unit
34
37
 
35
38
  // Fetch control.
36
39
  let priorityOrder: t => array<ChainState.t>