envio 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/index.d.ts +3 -0
  2. package/package.json +6 -6
  3. package/src/Batch.res +111 -78
  4. package/src/Batch.res.mjs +53 -46
  5. package/src/BatchProcessing.res +1 -11
  6. package/src/BatchProcessing.res.mjs +1 -4
  7. package/src/ChainState.res +72 -19
  8. package/src/ChainState.res.mjs +50 -12
  9. package/src/ChainState.resi +5 -0
  10. package/src/Config.res +58 -37
  11. package/src/Config.res.mjs +63 -53
  12. package/src/CrossChainState.res +49 -52
  13. package/src/CrossChainState.res.mjs +25 -23
  14. package/src/CrossChainState.resi +6 -13
  15. package/src/EffectState.res +13 -4
  16. package/src/EffectState.resi +7 -1
  17. package/src/EventProcessing.res +6 -6
  18. package/src/EventProcessing.res.mjs +6 -6
  19. package/src/FetchState.res +14 -0
  20. package/src/FetchState.res.mjs +14 -0
  21. package/src/FinalizeBackfill.res +16 -12
  22. package/src/FinalizeBackfill.res.mjs +3 -3
  23. package/src/HistoryPolicy.res +46 -0
  24. package/src/HistoryPolicy.res.mjs +50 -0
  25. package/src/InMemoryStore.res +30 -32
  26. package/src/InMemoryStore.res.mjs +33 -28
  27. package/src/IndexerState.res +66 -45
  28. package/src/IndexerState.res.mjs +57 -29
  29. package/src/IndexerState.resi +8 -13
  30. package/src/Internal.res +4 -2
  31. package/src/LoadLayer.res +11 -4
  32. package/src/LoadLayer.res.mjs +8 -5
  33. package/src/Main.res +25 -24
  34. package/src/Main.res.mjs +24 -25
  35. package/src/Metrics.res +49 -1
  36. package/src/Metrics.res.mjs +26 -3
  37. package/src/Persistence.res +55 -16
  38. package/src/Persistence.res.mjs +18 -3
  39. package/src/PgStorage.res +194 -118
  40. package/src/PgStorage.res.mjs +132 -72
  41. package/src/PruneStaleHistory.res +9 -15
  42. package/src/PruneStaleHistory.res.mjs +11 -20
  43. package/src/Rollback.res +9 -14
  44. package/src/Rollback.res.mjs +5 -3
  45. package/src/Sink.res +7 -5
  46. package/src/Sink.res.mjs +4 -4
  47. package/src/Staging.res +298 -0
  48. package/src/Staging.res.mjs +257 -0
  49. package/src/TestIndexer.res +5 -5
  50. package/src/TestIndexer.res.mjs +7 -6
  51. package/src/UserContext.res +7 -4
  52. package/src/UserContext.res.mjs +5 -2
  53. package/src/Utils.res +9 -0
  54. package/src/Utils.res.mjs +9 -0
  55. package/src/Writing.res +34 -20
  56. package/src/Writing.res.mjs +15 -20
  57. package/src/bindings/ClickHouse.res +41 -34
  58. package/src/bindings/ClickHouse.res.mjs +36 -30
  59. package/src/bindings/ClickHouseSink.res +45 -186
  60. package/src/bindings/ClickHouseSink.res.mjs +21 -174
  61. package/src/db/CheckpointSequence.res +109 -0
  62. package/src/db/CheckpointSequence.res.mjs +99 -0
  63. package/src/db/EntityHistory.res +6 -5
  64. package/src/db/EntityHistory.res.mjs +5 -5
  65. package/src/db/Frontier.res +86 -0
  66. package/src/db/Frontier.res.mjs +126 -0
  67. package/src/db/InternalTable.res +145 -27
  68. package/src/db/InternalTable.res.mjs +115 -29
  69. package/src/db/RollbackFloors.res +34 -41
  70. package/src/db/RollbackFloors.res.mjs +27 -71
  71. package/src/sources/BlockStore.res +3 -0
  72. package/src/sources/EvmHyperSyncSource.res +3 -1
  73. package/src/sources/EvmHyperSyncSource.res.mjs +2 -1
  74. package/src/sources/HyperSync.res +2 -0
  75. package/src/sources/HyperSync.res.mjs +1 -0
  76. package/src/sources/HyperSync.resi +1 -0
  77. package/src/sources/HyperSyncClient.res +3 -0
  78. package/src/sources/RequestStat.res +8 -1
  79. package/src/sources/Source.res +4 -3
  80. package/src/sources/SourceManager.res +30 -10
  81. package/src/sources/SourceManager.res.mjs +24 -7
  82. package/src/sources/SourceManager.resi +2 -0
  83. package/src/tui/Tui.res +56 -143
  84. package/src/tui/Tui.res.mjs +70 -166
  85. package/src/tui/components/TuiData.res +61 -16
  86. package/src/tui/components/TuiData.res.mjs +53 -15
  87. package/svm.schema.json +0 -7
  88. package/src/db/CheckpointBounds.res +0 -53
  89. package/src/db/CheckpointBounds.res.mjs +0 -44
@@ -21,6 +21,7 @@ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js
21
21
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
22
22
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
23
23
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
24
+ import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
24
25
  import * as EventConfigBuilder from "./EventConfigBuilder.res.mjs";
25
26
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
26
27
 
@@ -436,6 +437,7 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
436
437
  description: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
437
438
  handlers: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
438
439
  isDev: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
440
+ isolatedChains: s.m(S$RescriptSchema.option(S$RescriptSchema.array(ChainId.schema))),
439
441
  fullBatchSize: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
440
442
  rollbackOnReorg: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
441
443
  saveFullHistory: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
@@ -460,6 +462,33 @@ function contractMappingOf(chainConfigs) {
460
462
  return ContractMapping.make(names);
461
463
  }
462
464
 
465
+ function getChain(config, chainId) {
466
+ if (ChainMap.has(config.chainMap, chainId)) {
467
+ return chainId;
468
+ } else {
469
+ return Stdlib_JsError.throwWithMessage("No chain with id " + ChainId.toString(chainId) + " found in config.yaml");
470
+ }
471
+ }
472
+
473
+ function isolate(config, chainIds) {
474
+ let shared = config.userEntities.filter(entityConfig => entityConfig.crossChain);
475
+ if (shared.length !== 0) {
476
+ Stdlib_JsError.throwWithMessage(`Only a schema whose entities are all per-chain can be split across processes. Shared across chains: ` + shared.map(entityConfig => entityConfig.name).join(", ") + `.`);
477
+ }
478
+ chainIds.forEach(chainId => {
479
+ getChain(config, chainId);
480
+ });
481
+ let chains = ChainMap.values(config.chainMap).filter(chain => chainIds.includes(chain.id));
482
+ let newrecord = {...config};
483
+ newrecord.isolated = true;
484
+ newrecord.defaultChain = chains[0];
485
+ newrecord.chainMap = ChainMap.fromArrayUnsafe(chains.map(chain => [
486
+ chain.id,
487
+ chain
488
+ ]));
489
+ return newrecord;
490
+ }
491
+
463
492
  function fromPublic(publicConfigJson) {
464
493
  let publicConfig;
465
494
  try {
@@ -755,38 +784,50 @@ function fromPublic(publicConfigJson) {
755
784
  name: Utils.$$String.capitalize(param[0]),
756
785
  handler: param[1].handler
757
786
  })) : [];
758
- return {
759
- name: publicConfig.name,
760
- description: publicConfig.description,
761
- handlers: Stdlib_Option.getOr(publicConfig.handlers, "src/handlers"),
787
+ let config_name = publicConfig.name;
788
+ let config_description = publicConfig.description;
789
+ let config_handlers = Stdlib_Option.getOr(publicConfig.handlers, "src/handlers");
790
+ let config_shouldRollbackOnReorg = Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true);
791
+ let config_shouldSaveFullHistory = Stdlib_Option.getOr(publicConfig.saveFullHistory, false);
792
+ let config_chainIdMode = Stdlib_Option.getOr(publicConfig.chainIdMode, "int32");
793
+ let config_contractMapping = contractMappingOf(chains);
794
+ let config_defaultChain = chains[0];
795
+ let config_enableRawEvents = Stdlib_Option.getOr(publicConfig.rawEvents, false);
796
+ let config_batchSize = Stdlib_Option.getOr(publicConfig.fullBatchSize, 5000);
797
+ let config_isDev = Stdlib_Option.getOr(publicConfig.isDev, false);
798
+ let config_checkpointSequence = CheckpointSequence.fromEntities(userEntities);
799
+ let config = {
800
+ name: config_name,
801
+ description: config_description,
802
+ handlers: config_handlers,
762
803
  contractHandlers: contractHandlers,
763
- shouldRollbackOnReorg: Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true),
764
- shouldSaveFullHistory: Stdlib_Option.getOr(publicConfig.saveFullHistory, false),
804
+ shouldRollbackOnReorg: config_shouldRollbackOnReorg,
805
+ shouldSaveFullHistory: config_shouldSaveFullHistory,
765
806
  defaultCrossChain: defaultCrossChain,
766
807
  storage: globalStorage,
767
- chainIdMode: Stdlib_Option.getOr(publicConfig.chainIdMode, "int32"),
808
+ chainIdMode: config_chainIdMode,
768
809
  chainMap: chainMap,
769
- contractMapping: contractMappingOf(chains),
770
- defaultChain: chains[0],
810
+ contractMapping: config_contractMapping,
811
+ defaultChain: config_defaultChain,
771
812
  ecosystem: ecosystem,
772
- enableRawEvents: Stdlib_Option.getOr(publicConfig.rawEvents, false),
813
+ enableRawEvents: config_enableRawEvents,
773
814
  maxAddrInPartition: Env.maxAddrInPartition,
774
815
  clientFilterAddressThreshold: Env.clientFilterAddressThreshold,
775
- batchSize: Stdlib_Option.getOr(publicConfig.fullBatchSize, 5000),
816
+ batchSize: config_batchSize,
776
817
  reorgThresholdReadyTolerance: 100,
777
818
  lowercaseAddresses: lowercaseAddresses,
778
- isDev: Stdlib_Option.getOr(publicConfig.isDev, false),
819
+ isDev: config_isDev,
820
+ isolated: false,
779
821
  userEntitiesByName: userEntitiesByName,
780
822
  userEntities: userEntities,
781
- allEnums: allEnums
823
+ allEnums: allEnums,
824
+ checkpointSequence: config_checkpointSequence
782
825
  };
783
- }
784
-
785
- function isIsolatedMultichain(config) {
786
- if (ChainMap.keys(config.chainMap).length > 1) {
787
- return config.userEntities.every(entityConfig => !entityConfig.crossChain);
826
+ let chainIds = publicConfig.isolatedChains;
827
+ if (chainIds !== undefined) {
828
+ return isolate(config, chainIds);
788
829
  } else {
789
- return false;
830
+ return config;
790
831
  }
791
832
  }
792
833
 
@@ -853,36 +894,6 @@ function getEventConfig(config, contractName, eventName, chainId) {
853
894
  });
854
895
  }
855
896
 
856
- function shouldSaveHistory(config, isInReorgThreshold, chainId) {
857
- if (config.shouldSaveFullHistory) {
858
- return true;
859
- } else if (config.shouldRollbackOnReorg && isInReorgThreshold) {
860
- if (chainId !== undefined && isIsolatedMultichain(config)) {
861
- return ChainMap.get(config.chainMap, Primitive_option.valFromOption(chainId)).maxReorgDepth > 0;
862
- } else {
863
- return true;
864
- }
865
- } else {
866
- return false;
867
- }
868
- }
869
-
870
- function shouldPruneHistory(config, isInReorgThreshold) {
871
- if (!config.shouldSaveFullHistory && config.shouldRollbackOnReorg) {
872
- return isInReorgThreshold;
873
- } else {
874
- return false;
875
- }
876
- }
877
-
878
- function getChain(config, chainId) {
879
- if (ChainMap.has(config.chainMap, chainId)) {
880
- return chainId;
881
- } else {
882
- return Stdlib_JsError.throwWithMessage("No chain with id " + ChainId.toString(chainId) + " found in config.yaml");
883
- }
884
- }
885
-
886
897
  let primedJson = {
887
898
  contents: undefined
888
899
  };
@@ -931,6 +942,7 @@ function stripSensitiveData(json) {
931
942
  };
932
943
  if (typeof cloned === "object" && cloned !== null && !Array.isArray(cloned)) {
933
944
  Utils.Dict.deleteInPlace(cloned, "isDev");
945
+ Utils.Dict.deleteInPlace(cloned, "isolatedChains");
934
946
  stripChains(cloned["evm"]);
935
947
  stripChains(cloned["fuel"]);
936
948
  stripChains(cloned["svm"]);
@@ -1159,14 +1171,12 @@ export {
1159
1171
  publicConfigStorageSchema,
1160
1172
  publicConfigSchema,
1161
1173
  contractMappingOf,
1174
+ getChain,
1175
+ isolate,
1162
1176
  fromPublic,
1163
- isIsolatedMultichain,
1164
1177
  normalizeUserAddress,
1165
1178
  normalizeSimulateAddress,
1166
1179
  getEventConfig,
1167
- shouldSaveHistory,
1168
- shouldPruneHistory,
1169
- getChain,
1170
1180
  prime,
1171
1181
  getPublicConfigJson,
1172
1182
  stripSensitiveData,
@@ -8,13 +8,13 @@ type t = {
8
8
  // Chain ids in a stable order, so the cross-chain loops iterate the chains
9
9
  // without allocating a values array on every tick.
10
10
  chainIds: array<ChainId.t>,
11
- // True once every chain has caught up to head/endBlock. Monotonic during a run.
11
+ // True once every chain this process drives has caught up to head/endBlock and
12
+ // the indexes they owe are committed. Monotonic during a run.
12
13
  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.
14
+ // True once those chains have caught up and there's nothing left to process,
15
+ // but before the deferred schema indexes and `ready_at` are committed. The gap
16
+ // between this and `isRealtime` is the FinalizingIndexes phase.
16
17
  mutable isCaughtUp: bool,
17
- mutable isInReorgThreshold: bool,
18
18
  // Indexer-wide fetch buffer pool (item count), shared across all chains.
19
19
  targetBufferSize: int,
20
20
  }
@@ -26,18 +26,12 @@ let calculateTargetBufferSize = () =>
26
26
  | None => 100_000
27
27
  }
28
28
 
29
- let make = (
30
- ~chainStates,
31
- ~isInReorgThreshold,
32
- ~isRealtime,
33
- ~targetBufferSize=calculateTargetBufferSize(),
34
- ): t => {
29
+ let make = (~chainStates, ~isRealtime, ~targetBufferSize=calculateTargetBufferSize()): t => {
35
30
  {
36
31
  chainStates,
37
32
  chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
38
33
  isRealtime,
39
34
  isCaughtUp: isRealtime,
40
- isInReorgThreshold,
41
35
  targetBufferSize,
42
36
  }
43
37
  }
@@ -51,10 +45,21 @@ let getChainState = (crossChainState: t, chainId) =>
51
45
 
52
46
  let chainStates = (crossChainState: t) => crossChainState.chainStates
53
47
  let isRealtime = (crossChainState: t) => crossChainState.isRealtime
48
+ let chainIds = (crossChainState: t) => crossChainState.chainIds
54
49
  let isCaughtUp = (crossChainState: t) => crossChainState.isCaughtUp
55
- let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
50
+ // Chains enter the threshold together. It is the run-wide reading; what a
51
+ // write keeps is decided per chain.
52
+ let isInReorgThreshold = (crossChainState: t) => {
53
+ let chainStates = crossChainState.chainStates->Dict.valuesToArray
54
+ chainStates->Utils.Array.notEmpty && chainStates->Array.every(ChainState.isInReorgThreshold)
55
+ }
56
56
  let targetBufferSize = (crossChainState: t) => crossChainState.targetBufferSize
57
57
 
58
+ // Whether each chain's writes still need history, keyed by chain id — what the
59
+ // history policy is built from.
60
+ let shouldSaveHistory = (crossChainState: t) =>
61
+ crossChainState.chainStates->Utils.Dict.mapValues(ChainState.shouldSaveHistory)
62
+
58
63
  // Ready-to-process items across every chain — the live draw against
59
64
  // targetBufferSize, which is a budget of processable events (items stuck behind
60
65
  // a gap don't count toward the goal of keeping ~targetBufferSize ready).
@@ -74,14 +79,20 @@ let nextItemIsNone = (crossChainState: t): bool =>
74
79
 
75
80
  // Each chain's safe checkpoint: the last one a reorg on that chain can no longer
76
81
  // reach, or None while it has nothing safe yet. A chain that can't be rolled
77
- // back (maxReorgDepth = 0) tracks none, and everything it has committed is safe.
78
- let getSafeCheckpointIdByChain = (crossChainState: t, ~committedCheckpointId) =>
82
+ // back (maxReorgDepth = 0) tracks none, and everything committed is safe from
83
+ // it: under one shared sequence that is the run's highest id, not the chain's
84
+ // own — an idle chain's would hold every other chain's prune back.
85
+ let getSafeCheckpointIdByChain = (
86
+ crossChainState: t,
87
+ ~sequence: CheckpointSequence.t,
88
+ ~committedFrontier,
89
+ ) =>
79
90
  crossChainState.chainIds->Array.map(chainId => {
80
91
  let cs = crossChainState->getChainState(chainId)
81
92
  (
82
93
  chainId,
83
94
  switch cs->ChainState.safeCheckpointTracking {
84
- | None => Some(committedCheckpointId)
95
+ | None => Some(sequence->CheckpointSequence.position(committedFrontier, ~chainId))
85
96
  | Some(tracking) =>
86
97
  tracking->SafeCheckpointTracking.getSafeCheckpointId(
87
98
  ~sourceBlockNumber=cs->ChainState.knownHeight,
@@ -94,18 +105,14 @@ let getSafeCheckpointIdByChain = (crossChainState: t, ~committedCheckpointId) =>
94
105
 
95
106
  let createBatch = (
96
107
  crossChainState: t,
97
- ~processedCheckpointId,
108
+ ~config: Config.t,
109
+ ~frontier,
98
110
  ~batchSizeTarget: int,
99
- ~isRollback: bool,
100
111
  ): Batch.t => {
101
112
  Batch.make(
102
- ~isInReorgThreshold=crossChainState.isInReorgThreshold,
103
- ~checkpointIdBeforeBatch=processedCheckpointId->BigInt.add(
104
- // Since for rollback we have a diff checkpoint id.
105
- // This is needed to currectly overwrite old state
106
- // in an append-only ClickHouse insert.
107
- isRollback ? 1n : 0n,
108
- ),
113
+ ~sequence=config.checkpointSequence,
114
+ ~history=config->HistoryPolicy.decide(~shouldSaveHistory=crossChainState->shouldSaveHistory),
115
+ ~frontier,
109
116
  ~chainsBeforeBatch=crossChainState.chainStates->Utils.Dict.mapValues(
110
117
  ChainState.toChainBeforeBatch,
111
118
  ),
@@ -123,8 +130,6 @@ let enterReorgThreshold = (crossChainState: t) => {
123
130
  ->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
124
131
  ->ChainState.enterReorgThreshold
125
132
  }
126
-
127
- crossChainState.isInReorgThreshold = true
128
133
  }
129
134
 
130
135
  // Commit each progressed chain's batch progress, then record whether the whole
@@ -146,17 +151,6 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
146
151
 
147
152
  crossChainState.isCaughtUp =
148
153
  crossChainState.isCaughtUp || (crossChainState->nextItemIsNone && everyChainCaughtUp.contents)
149
-
150
- // A run resumed with every chain already stamped ready needs no finalize —
151
- // the indexes were committed together with those stamps.
152
- let allChainsReady = ref(true)
153
- for i in 0 to chainIds->Array.length - 1 {
154
- if !(crossChainState->getChainState(chainIds->Array.getUnsafe(i))->ChainState.isReady) {
155
- allChainsReady := false
156
- }
157
- }
158
-
159
- crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
160
154
  }
161
155
 
162
156
  // Every chain has buffered up to its head (or endblock) with nothing
@@ -191,29 +185,25 @@ let markCaughtUpIfSettled = (crossChainState: t) =>
191
185
  // may have moved on — at which point no chain looks at head any more and the
192
186
  // indexes it still owes would wait out another whole backfill. Deciding here,
193
187
  // before any source request, keeps that debt tied to the progress that was
194
- // actually committed. Skipped once every chain carries `ready_at`: those indexes
195
- // were committed together with the stamps.
188
+ // actually committed. Skipped when the run resumed realtime, which is exactly
189
+ // the case where the indexes are already committed and nothing is owed.
196
190
  let markCaughtUpOnResume = (crossChainState: t) => {
197
191
  let everyChainCaughtUp = ref(crossChainState.chainIds->Array.length > 0)
198
- let everyChainReady = ref(true)
199
192
  for i in 0 to crossChainState.chainIds->Array.length - 1 {
200
193
  let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
201
194
  if !(cs->ChainState.isDurablyCaughtUp) {
202
195
  everyChainCaughtUp := false
203
196
  }
204
- if !(cs->ChainState.isReady) {
205
- everyChainReady := false
206
- }
207
197
  }
208
198
 
209
- if everyChainCaughtUp.contents && !everyChainReady.contents && crossChainState->nextItemIsNone {
199
+ if everyChainCaughtUp.contents && !crossChainState.isRealtime && crossChainState->nextItemIsNone {
210
200
  crossChainState.isCaughtUp = true
211
201
  }
212
202
  }
213
203
 
214
- // Concludes the FinalizingIndexes phase: stamps every chain with the `ready_at`
215
- // already committed alongside the deferred schema indexes and switches the
216
- // indexer to realtime.
204
+ // Concludes the FinalizingIndexes phase: stamps every chain this process drives
205
+ // with the `ready_at` already committed alongside their deferred schema indexes,
206
+ // and switches the indexer to realtime.
217
207
  let markReady = (crossChainState: t, ~readyAt) => {
218
208
  for i in 0 to crossChainState.chainIds->Array.length - 1 {
219
209
  crossChainState
@@ -267,6 +257,10 @@ let idleOrWaitAction = (cs: ChainState.t) =>
267
257
  ? FetchState.NothingToQuery
268
258
  : FetchState.WaitingForNewBlock
269
259
 
260
+ // How far past the alignment anchor's frontier progress a follower may fetch,
261
+ // as a fraction of its own alignable range.
262
+ let alignmentMargin = 0.2
263
+
270
264
  // Dispatch a fetch tick across the whole indexer from one shared pool of
271
265
  // ~targetBufferSize ready events, as a waterfall: visit chains furthest-behind
272
266
  // first, hand each the budget remaining at that point (plus its own
@@ -350,11 +344,14 @@ let checkAndFetch = async (
350
344
  (isCold ? Pervasives.min(remaining.contents, coldChainBudget) : remaining.contents) +.
351
345
  cs->ChainState.pendingBudget
352
346
  let maxTargetBlock = switch alignment {
353
- // 10% margin past the anchor's line: chains whose progress tracks the
347
+ // 20% margin past the anchor's line: chains whose progress tracks the
354
348
  // anchor closely would otherwise flap in and out of the clamp on every
355
- // small frontier move, stalling their pipeline every other tick.
349
+ // small frontier move, stalling their pipeline every other tick. The
350
+ // margin is also the headroom a follower keeps buffered while the anchor
351
+ // is mid-fetch, so it has to outlast a slow anchor response, not just
352
+ // absorb frontier jitter.
356
353
  | Some((anchorChainId, progress)) if anchorChainId !== chainId =>
357
- Some(cs->ChainState.blockAtProgress(~progress=progress +. 0.1))
354
+ Some(cs->ChainState.blockAtProgress(~progress=progress +. alignmentMargin))
358
355
  | _ => None
359
356
  }
360
357
  switch cs->ChainState.getNextQuery(~chainTargetItems, ~maxTargetBlock?) {
@@ -404,7 +401,7 @@ let checkAndFetch = async (
404
401
  switch actionByChain->ChainId.Dict.dangerouslyGetNonOption(chainId) {
405
402
  | Some(NothingToQuery)
406
403
  | None => ()
407
- | Some(action) => promises->Array.push(dispatchChain(~chainId=chainId, ~action))
404
+ | Some(action) => promises->Array.push(dispatchChain(~chainId, ~action))
408
405
  }
409
406
  }
410
407
  let _ = await promises->Promise.all
@@ -6,8 +6,10 @@ import * as Utils from "./Utils.res.mjs";
6
6
  import * as Logging from "./Logging.res.mjs";
7
7
  import * as ChainState from "./ChainState.res.mjs";
8
8
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
+ import * as HistoryPolicy from "./HistoryPolicy.res.mjs";
9
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
11
  import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
12
+ import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
11
13
  import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
12
14
 
13
15
  function calculateTargetBufferSize() {
@@ -18,7 +20,7 @@ function calculateTargetBufferSize() {
18
20
  }
19
21
  }
20
22
 
21
- function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt) {
23
+ function make(chainStates, isRealtime, targetBufferSizeOpt) {
22
24
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
23
25
  Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
24
26
  );
@@ -27,7 +29,6 @@ function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt)
27
29
  chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
28
30
  isRealtime: isRealtime,
29
31
  isCaughtUp: isRealtime,
30
- isInReorgThreshold: isInReorgThreshold,
31
32
  targetBufferSize: targetBufferSize
32
33
  };
33
34
  }
@@ -40,18 +41,31 @@ function isRealtime(crossChainState) {
40
41
  return crossChainState.isRealtime;
41
42
  }
42
43
 
44
+ function chainIds(crossChainState) {
45
+ return crossChainState.chainIds;
46
+ }
47
+
43
48
  function isCaughtUp(crossChainState) {
44
49
  return crossChainState.isCaughtUp;
45
50
  }
46
51
 
47
52
  function isInReorgThreshold(crossChainState) {
48
- return crossChainState.isInReorgThreshold;
53
+ let chainStates = Object.values(crossChainState.chainStates);
54
+ if (Utils.$$Array.notEmpty(chainStates)) {
55
+ return chainStates.every(ChainState.isInReorgThreshold);
56
+ } else {
57
+ return false;
58
+ }
49
59
  }
50
60
 
51
61
  function targetBufferSize(crossChainState) {
52
62
  return crossChainState.targetBufferSize;
53
63
  }
54
64
 
65
+ function shouldSaveHistory(crossChainState) {
66
+ return Utils.Dict.mapValues(crossChainState.chainStates, ChainState.shouldSaveHistory);
67
+ }
68
+
55
69
  function totalReadyCount(crossChainState) {
56
70
  let total = 0;
57
71
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
@@ -65,21 +79,19 @@ function nextItemIsNone(crossChainState) {
65
79
  return !Object.values(crossChainState.chainStates).some(ChainState.hasReadyItem);
66
80
  }
67
81
 
68
- function getSafeCheckpointIdByChain(crossChainState, committedCheckpointId) {
82
+ function getSafeCheckpointIdByChain(crossChainState, sequence, committedFrontier) {
69
83
  return crossChainState.chainIds.map(chainId => {
70
84
  let cs = crossChainState.chainStates[chainId];
71
85
  let tracking = ChainState.safeCheckpointTracking(cs);
72
86
  return [
73
87
  chainId,
74
- tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : committedCheckpointId
88
+ tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : CheckpointSequence.position(sequence, committedFrontier, chainId)
75
89
  ];
76
90
  });
77
91
  }
78
92
 
79
- function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
80
- return Batch.make(processedCheckpointId + (
81
- isRollback ? 1n : 0n
82
- ), Utils.Dict.mapValues(crossChainState.chainStates, ChainState.toChainBeforeBatch), batchSizeTarget, crossChainState.isInReorgThreshold);
93
+ function createBatch(crossChainState, config, frontier, batchSizeTarget) {
94
+ return Batch.make(config.checkpointSequence, frontier, Utils.Dict.mapValues(crossChainState.chainStates, ChainState.toChainBeforeBatch), batchSizeTarget, HistoryPolicy.decide(config, shouldSaveHistory(crossChainState)));
83
95
  }
84
96
 
85
97
  function enterReorgThreshold(crossChainState) {
@@ -87,7 +99,6 @@ function enterReorgThreshold(crossChainState) {
87
99
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
88
100
  ChainState.enterReorgThreshold(crossChainState.chainStates[crossChainState.chainIds[i]]);
89
101
  }
90
- crossChainState.isInReorgThreshold = true;
91
102
  }
92
103
 
93
104
  function applyBatchProgress(crossChainState, batch, blockTimestampName) {
@@ -101,13 +112,6 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
101
112
  }
102
113
  }
103
114
  crossChainState.isCaughtUp = crossChainState.isCaughtUp || nextItemIsNone(crossChainState) && everyChainCaughtUp;
104
- let allChainsReady = true;
105
- for (let i$1 = 0, i_finish$1 = chainIds.length; i$1 < i_finish$1; ++i$1) {
106
- if (!ChainState.isReady(crossChainState.chainStates[chainIds[i$1]])) {
107
- allChainsReady = false;
108
- }
109
- }
110
- crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady;
111
115
  }
112
116
 
113
117
  function isSettledAtHead(crossChainState) {
@@ -129,17 +133,13 @@ function markCaughtUpIfSettled(crossChainState) {
129
133
 
130
134
  function markCaughtUpOnResume(crossChainState) {
131
135
  let everyChainCaughtUp = crossChainState.chainIds.length !== 0;
132
- let everyChainReady = true;
133
136
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
134
137
  let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
135
138
  if (!ChainState.isDurablyCaughtUp(cs)) {
136
139
  everyChainCaughtUp = false;
137
140
  }
138
- if (!ChainState.isReady(cs)) {
139
- everyChainReady = false;
140
- }
141
141
  }
142
- if (everyChainCaughtUp && !everyChainReady && nextItemIsNone(crossChainState)) {
142
+ if (everyChainCaughtUp && !crossChainState.isRealtime && nextItemIsNone(crossChainState)) {
143
143
  crossChainState.isCaughtUp = true;
144
144
  return;
145
145
  }
@@ -199,7 +199,7 @@ async function checkAndFetch(crossChainState, dispatchChain) {
199
199
  let chainTargetItems = (
200
200
  isCold ? Primitive_float.min(remaining.contents, minimumAdmissionBudget) : remaining.contents
201
201
  ) + ChainState.pendingBudget(cs);
202
- let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.1) : undefined;
202
+ let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.2) : undefined;
203
203
  let action = ChainState.getNextQuery(cs, chainTargetItems, maxTargetBlock);
204
204
  if (typeof action !== "object") {
205
205
  if (action === "WaitingForNewBlock") {
@@ -249,9 +249,11 @@ export {
249
249
  make,
250
250
  chainStates,
251
251
  isRealtime,
252
+ chainIds,
252
253
  isCaughtUp,
253
254
  isInReorgThreshold,
254
255
  targetBufferSize,
256
+ shouldSaveHistory,
255
257
  nextItemIsNone,
256
258
  getSafeCheckpointIdByChain,
257
259
  createBatch,
@@ -5,34 +5,27 @@ type t
5
5
 
6
6
  let calculateTargetBufferSize: unit => int
7
7
 
8
- let make: (
9
- ~chainStates: dict<ChainState.t>,
10
- ~isInReorgThreshold: bool,
11
- ~isRealtime: bool,
12
- ~targetBufferSize: int=?,
13
- ) => t
8
+ let make: (~chainStates: dict<ChainState.t>, ~isRealtime: bool, ~targetBufferSize: int=?) => t
14
9
 
15
10
  // Accessors.
16
11
  let chainStates: t => dict<ChainState.t>
17
12
  let isRealtime: t => bool
13
+ let chainIds: t => array<ChainId.t>
18
14
  let isCaughtUp: t => bool
19
15
  let isInReorgThreshold: t => bool
20
16
  let targetBufferSize: t => int
17
+ let shouldSaveHistory: t => dict<bool>
21
18
 
22
19
  // Derived (pure).
23
20
  let nextItemIsNone: t => bool
24
21
  let getSafeCheckpointIdByChain: (
25
22
  t,
26
- ~committedCheckpointId: Internal.checkpointId,
23
+ ~sequence: CheckpointSequence.t,
24
+ ~committedFrontier: Frontier.t,
27
25
  ) => array<(ChainId.t, option<Internal.checkpointId>)>
28
26
 
29
27
  // Cross-chain transitions.
30
- let createBatch: (
31
- t,
32
- ~processedCheckpointId: Internal.checkpointId,
33
- ~batchSizeTarget: int,
34
- ~isRollback: bool,
35
- ) => Batch.t
28
+ let createBatch: (t, ~config: Config.t, ~frontier: Frontier.t, ~batchSizeTarget: int) => Batch.t
36
29
  let enterReorgThreshold: t => unit
37
30
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
38
31
  let markCaughtUpIfSettled: t => unit
@@ -36,15 +36,24 @@ type effectStats = {
36
36
  mutable hasCache: bool,
37
37
  }
38
38
 
39
+ // A handler output, stamped with the checkpoint that produced it on the chain
40
+ // whose handler ran it. A cross-chain effect's entries come from several
41
+ // chains, and under per-chain sequences an id only compares against its own
42
+ // chain's committed one.
43
+ type cacheEntry = {
44
+ output: Internal.effectOutput,
45
+ chainId: ChainId.t,
46
+ checkpointId: Internal.checkpointId,
47
+ }
48
+
39
49
  type effectCacheInMemTable = {
40
50
  // Cache keys whose handler output is persisted on the next write. Drained
41
51
  // each write; eviction is driven by the per-entry checkpointId instead.
42
52
  mutable idsToStore: array<string>,
43
53
  mutable invalidationsCount: int,
44
- // Each entry is stamped with the checkpoint that referenced it (or
45
- // loadedFromDbCheckpointId for db reads), so committed entries can be
46
- // dropped once persisted/re-derivable, mirroring entity changes.
47
- mutable dict: dict<Change.t<Internal.effectOutput>>,
54
+ // Committed entries are dropped once persisted or re-derivable, mirroring
55
+ // entity changes; a db read is stamped loadedFromDbCheckpointId.
56
+ mutable dict: dict<cacheEntry>,
48
57
  mutable changesCount: float,
49
58
  effect: Internal.effect,
50
59
  // The scope this cache is for and its resolved db table (the address). The
@@ -11,10 +11,16 @@ type effectRateLimitState = {
11
11
  // toMetrics.
12
12
  type effectStats
13
13
 
14
+ type cacheEntry = {
15
+ output: Internal.effectOutput,
16
+ chainId: ChainId.t,
17
+ checkpointId: Internal.checkpointId,
18
+ }
19
+
14
20
  type effectCacheInMemTable = {
15
21
  mutable idsToStore: array<string>,
16
22
  mutable invalidationsCount: int,
17
- mutable dict: dict<Change.t<Internal.effectOutput>>,
23
+ mutable dict: dict<cacheEntry>,
18
24
  mutable changesCount: float,
19
25
  effect: Internal.effect,
20
26
  scope: Internal.chainScope,
@@ -157,9 +157,9 @@ let preloadBatchOrThrow = async (
157
157
 
158
158
  for checkpointIdx in 0 to batch.checkpointIds->Array.length - 1 {
159
159
  let checkpointId = batch.checkpointIds->Array.getUnsafe(checkpointIdx)
160
- let checkpointEventsProcessed = batch.checkpointEventsProcessed->Array.getUnsafe(checkpointIdx)
160
+ let checkpointItemsCount = batch.checkpointItemsCount->Array.getUnsafe(checkpointIdx)
161
161
 
162
- for idx in 0 to checkpointEventsProcessed - 1 {
162
+ for idx in 0 to checkpointItemsCount - 1 {
163
163
  let item = batch.items->Array.getUnsafe(itemIdx.contents + idx)
164
164
  switch item {
165
165
  | Event(_) =>
@@ -233,7 +233,7 @@ let preloadBatchOrThrow = async (
233
233
  }
234
234
  }
235
235
 
236
- itemIdx := itemIdx.contents + checkpointEventsProcessed
236
+ itemIdx := itemIdx.contents + checkpointItemsCount
237
237
  }
238
238
 
239
239
  let _ = await Promise.all(promises)
@@ -251,9 +251,9 @@ let runBatchHandlersOrThrow = async (
251
251
 
252
252
  for checkpointIdx in 0 to batch.checkpointIds->Array.length - 1 {
253
253
  let checkpointId = batch.checkpointIds->Array.getUnsafe(checkpointIdx)
254
- let checkpointEventsProcessed = batch.checkpointEventsProcessed->Array.getUnsafe(checkpointIdx)
254
+ let checkpointItemsCount = batch.checkpointItemsCount->Array.getUnsafe(checkpointIdx)
255
255
 
256
- for idx in 0 to checkpointEventsProcessed - 1 {
256
+ for idx in 0 to checkpointItemsCount - 1 {
257
257
  let item = batch.items->Array.getUnsafe(itemIdx.contents + idx)
258
258
 
259
259
  await runHandlerOrThrow(
@@ -266,7 +266,7 @@ let runBatchHandlersOrThrow = async (
266
266
  ~chains,
267
267
  )
268
268
  }
269
- itemIdx := itemIdx.contents + checkpointEventsProcessed
269
+ itemIdx := itemIdx.contents + checkpointItemsCount
270
270
  }
271
271
  }
272
272