envio 3.0.2 → 3.1.0-rc.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.
Files changed (101) hide show
  1. package/README.md +0 -1
  2. package/evm.schema.json +15 -8
  3. package/fuel.schema.json +19 -12
  4. package/index.d.ts +0 -2
  5. package/package.json +6 -7
  6. package/rescript.json +1 -1
  7. package/src/Batch.res +4 -214
  8. package/src/Batch.res.mjs +6 -165
  9. package/src/ChainFetcher.res +12 -28
  10. package/src/ChainFetcher.res.mjs +8 -17
  11. package/src/ChainManager.res +10 -9
  12. package/src/ChainManager.res.mjs +6 -10
  13. package/src/Config.res +9 -25
  14. package/src/Config.res.mjs +17 -27
  15. package/src/Core.res +7 -0
  16. package/src/Ctx.res +1 -0
  17. package/src/Env.res +0 -8
  18. package/src/Env.res.mjs +0 -6
  19. package/src/EventConfigBuilder.res +13 -123
  20. package/src/EventConfigBuilder.res.mjs +6 -73
  21. package/src/EventProcessing.res +5 -29
  22. package/src/EventProcessing.res.mjs +11 -20
  23. package/src/EventUtils.res +0 -27
  24. package/src/EventUtils.res.mjs +0 -24
  25. package/src/FetchState.res +2 -15
  26. package/src/FetchState.res.mjs +3 -18
  27. package/src/GlobalState.res +26 -39
  28. package/src/GlobalState.res.mjs +12 -40
  29. package/src/HandlerLoader.res +6 -5
  30. package/src/HandlerLoader.res.mjs +27 -9
  31. package/src/HandlerRegister.res +1 -12
  32. package/src/HandlerRegister.res.mjs +1 -6
  33. package/src/HandlerRegister.resi +1 -1
  34. package/src/Hasura.res +96 -32
  35. package/src/Hasura.res.mjs +93 -38
  36. package/src/InMemoryStore.res +205 -45
  37. package/src/InMemoryStore.res.mjs +157 -40
  38. package/src/InMemoryTable.res +165 -249
  39. package/src/InMemoryTable.res.mjs +156 -227
  40. package/src/Internal.res +10 -34
  41. package/src/Internal.res.mjs +9 -3
  42. package/src/LoadLayer.res +5 -5
  43. package/src/LoadLayer.res.mjs +5 -5
  44. package/src/LogSelection.res +15 -19
  45. package/src/LogSelection.res.mjs +5 -6
  46. package/src/Main.res +4 -6
  47. package/src/Main.res.mjs +26 -15
  48. package/src/Persistence.res +7 -132
  49. package/src/Persistence.res.mjs +1 -102
  50. package/src/PgStorage.res +57 -40
  51. package/src/PgStorage.res.mjs +60 -34
  52. package/src/ReorgDetection.res +35 -58
  53. package/src/ReorgDetection.res.mjs +21 -29
  54. package/src/SimulateItems.res.mjs +21 -3
  55. package/src/Sink.res +2 -2
  56. package/src/Sink.res.mjs +1 -1
  57. package/src/TableIndices.res +9 -2
  58. package/src/TableIndices.res.mjs +7 -1
  59. package/src/TestIndexer.res +53 -60
  60. package/src/TestIndexer.res.mjs +77 -63
  61. package/src/TestIndexerProxyStorage.res +4 -14
  62. package/src/TestIndexerProxyStorage.res.mjs +1 -5
  63. package/src/UserContext.res +2 -4
  64. package/src/UserContext.res.mjs +4 -5
  65. package/src/Utils.res +0 -2
  66. package/src/Utils.res.mjs +0 -3
  67. package/src/bindings/ClickHouse.res +45 -38
  68. package/src/bindings/ClickHouse.res.mjs +16 -17
  69. package/src/bindings/Vitest.res +3 -0
  70. package/src/db/InternalTable.res +59 -18
  71. package/src/db/InternalTable.res.mjs +82 -51
  72. package/src/db/Table.res +9 -2
  73. package/src/db/Table.res.mjs +10 -7
  74. package/src/sources/EnvioApiClient.res +15 -0
  75. package/src/sources/EnvioApiClient.res.mjs +24 -0
  76. package/src/sources/EvmChain.res +32 -10
  77. package/src/sources/EvmChain.res.mjs +31 -5
  78. package/src/sources/HyperFuelSource.res +15 -58
  79. package/src/sources/HyperFuelSource.res.mjs +20 -39
  80. package/src/sources/HyperSync.res +54 -100
  81. package/src/sources/HyperSync.res.mjs +67 -96
  82. package/src/sources/HyperSync.resi +4 -22
  83. package/src/sources/HyperSyncClient.res +70 -247
  84. package/src/sources/HyperSyncClient.res.mjs +47 -46
  85. package/src/sources/HyperSyncSource.res +94 -166
  86. package/src/sources/HyperSyncSource.res.mjs +100 -127
  87. package/src/sources/RpcSource.res +43 -22
  88. package/src/sources/RpcSource.res.mjs +50 -35
  89. package/src/sources/SimulateSource.res +1 -7
  90. package/src/sources/SimulateSource.res.mjs +1 -7
  91. package/src/sources/Source.res +10 -1
  92. package/src/sources/Source.res.mjs +3 -0
  93. package/src/sources/SourceManager.res +177 -8
  94. package/src/sources/SourceManager.res.mjs +141 -3
  95. package/src/sources/SourceManager.resi +19 -0
  96. package/src/tui/Tui.res +44 -6
  97. package/src/tui/Tui.res.mjs +56 -8
  98. package/src/tui/components/TuiData.res +3 -0
  99. package/svm.schema.json +11 -4
  100. package/src/sources/HyperSyncJsonApi.res +0 -390
  101. package/src/sources/HyperSyncJsonApi.res.mjs +0 -237
@@ -19,6 +19,7 @@ import * as Persistence from "./Persistence.res.mjs";
19
19
  import * as ChainFetcher from "./ChainFetcher.res.mjs";
20
20
  import * as EntityHistory from "./db/EntityHistory.res.mjs";
21
21
  import * as InternalTable from "./db/InternalTable.res.mjs";
22
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
22
23
  import * as Child_process from "child_process";
23
24
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
24
25
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
@@ -125,7 +126,8 @@ function getEntityHistory(entityConfig) {
125
126
  isPrimaryKey: true,
126
127
  isIndex: field$1.isIndex,
127
128
  linkedEntity: field$1.linkedEntity,
128
- defaultValue: field$1.defaultValue
129
+ defaultValue: field$1.defaultValue,
130
+ description: field$1.description
129
131
  }
130
132
  };
131
133
  } else {
@@ -140,19 +142,20 @@ function getEntityHistory(entityConfig) {
140
142
  isPrimaryKey: field$1.isPrimaryKey,
141
143
  isIndex: false,
142
144
  linkedEntity: field$1.linkedEntity,
143
- defaultValue: field$1.defaultValue
145
+ defaultValue: field$1.defaultValue,
146
+ description: field$1.description
144
147
  }
145
148
  };
146
149
  }
147
150
  });
148
- let actionField = Table.mkField(EntityHistory.changeFieldName, EntityHistory.changeFieldType, S$RescriptSchema.never, undefined, undefined, undefined, undefined, undefined, undefined);
149
- let checkpointIdField = Table.mkField(EntityHistory.checkpointIdFieldName, EntityHistory.checkpointIdFieldType, EntityHistory.unsafeCheckpointIdSchema, undefined, undefined, undefined, true, undefined, undefined);
151
+ let actionField = Table.mkField(EntityHistory.changeFieldName, EntityHistory.changeFieldType, S$RescriptSchema.never, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
152
+ let checkpointIdField = Table.mkField(EntityHistory.checkpointIdFieldName, EntityHistory.checkpointIdFieldType, EntityHistory.unsafeCheckpointIdSchema, undefined, undefined, undefined, true, undefined, undefined, undefined);
150
153
  let entityTableName = entityConfig.table.tableName;
151
154
  let historyTableName = EntityHistory.historyTableName(entityTableName, entityConfig.index);
152
155
  let table = Table.mkTable(historyTableName, undefined, Belt_Array.concat(dataFields, [
153
156
  checkpointIdField,
154
157
  actionField
155
- ]));
158
+ ]), undefined);
156
159
  let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema);
157
160
  let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema);
158
161
  let cache$1 = {
@@ -506,7 +509,7 @@ function executeSet(sql, items, dbFunction) {
506
509
  }
507
510
  }
508
511
 
509
- async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, escapeTables) {
512
+ async function writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, escapeTables) {
510
513
  try {
511
514
  let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold);
512
515
  let specificError = {
@@ -514,41 +517,54 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpo
514
517
  };
515
518
  let setRawEvents = __x => executeSet(__x, rawEvents, (sql, items) => setOrThrow(sql, items, InternalTable.RawEvents.table, InternalTable.RawEvents.schema, pgSchema));
516
519
  let setEntities = Belt_Array.map(updatedEntities, param => {
517
- let updates = param.updates;
518
520
  let entityConfig = param.entityConfig;
519
521
  let entitiesToSet = [];
520
522
  let idsToDelete = [];
521
- updates.forEach(row => {
522
- let match = row.latestChange;
523
+ let diffCheckpointId = Stdlib_Option.map(rollback, r => r.diffCheckpointId);
524
+ let batchSetUpdates = [];
525
+ let batchDeleteEntityIds = [];
526
+ let batchDeleteCheckpointIds = [];
527
+ let idsWithDiff = new Set();
528
+ let latestChangeById = {};
529
+ let orderedIds = [];
530
+ Belt_Array.forEach(param.changes, change => {
531
+ let entityId = change.entityId;
532
+ if (Stdlib_Option.isNone(latestChangeById[entityId])) {
533
+ orderedIds.push(entityId);
534
+ }
535
+ latestChangeById[entityId] = change;
536
+ if (!shouldSaveHistory) {
537
+ return;
538
+ }
539
+ if (change.checkpointId === diffCheckpointId) {
540
+ idsWithDiff.add(entityId);
541
+ return;
542
+ }
543
+ if (change.type === "SET") {
544
+ batchSetUpdates.push(change);
545
+ return;
546
+ }
547
+ batchDeleteEntityIds.push(change.entityId);
548
+ batchDeleteCheckpointIds.push(change.checkpointId);
549
+ });
550
+ let backfillHistoryIds = new Set();
551
+ Belt_Array.forEach(orderedIds, entityId => {
552
+ let match = latestChangeById[entityId];
523
553
  if (match.type === "SET") {
524
554
  entitiesToSet.push(match.entity);
555
+ } else {
556
+ idsToDelete.push(match.entityId);
557
+ }
558
+ if (shouldSaveHistory && !idsWithDiff.has(entityId)) {
559
+ backfillHistoryIds.add(entityId);
525
560
  return;
526
561
  }
527
- idsToDelete.push(match.entityId);
528
562
  });
529
563
  let shouldRemoveInvalidUtf8 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables).has(entityConfig.table) : false;
530
564
  return async sql => {
531
565
  try {
532
566
  let promises = [];
533
567
  if (shouldSaveHistory) {
534
- let backfillHistoryIds = new Set();
535
- let batchSetUpdates = [];
536
- let batchDeleteCheckpointIds = [];
537
- let batchDeleteEntityIds = [];
538
- updates.forEach(update => {
539
- let containsRollbackDiffChange = update.containsRollbackDiffChange;
540
- update.history.forEach(change => {
541
- if (!containsRollbackDiffChange) {
542
- backfillHistoryIds.add(change.entityId);
543
- }
544
- if (change.type === "SET") {
545
- batchSetUpdates.push(change);
546
- return;
547
- }
548
- batchDeleteEntityIds.push(change.entityId);
549
- batchDeleteCheckpointIds.push(change.checkpointId);
550
- });
551
- });
552
568
  if (backfillHistoryIds.size !== 0) {
553
569
  await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.name, entityConfig.index, Array.from(backfillHistoryIds));
554
570
  }
@@ -615,11 +631,17 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpo
615
631
  }
616
632
  };
617
633
  });
618
- let rollbackTables = rollbackTargetCheckpointId !== undefined ? sql => {
634
+ let rollbackTables;
635
+ if (rollback !== undefined) {
636
+ let rollbackTargetCheckpointId = rollback.targetCheckpointId;
637
+ rollbackTables = sql => {
619
638
  let promises = allEntities.map(entityConfig => EntityHistory.rollback(sql, pgSchema, entityConfig.name, entityConfig.index, rollbackTargetCheckpointId));
620
639
  promises.push(InternalTable.Checkpoints.rollback(sql, pgSchema, rollbackTargetCheckpointId));
621
640
  return Promise.all(promises);
622
- } : undefined;
641
+ };
642
+ } else {
643
+ rollbackTables = undefined;
644
+ }
623
645
  try {
624
646
  await Promise.all([
625
647
  sql.begin(async sql => {
@@ -665,7 +687,7 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpo
665
687
  if (exn$1.RE_EXN_ID === PgEncodingError) {
666
688
  let escapeTables$1 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables) : new Set();
667
689
  escapeTables$1.add(exn$1.table);
668
- return await writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, Primitive_option.some(escapeTables$1));
690
+ return await writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, Primitive_option.some(escapeTables$1));
669
691
  }
670
692
  throw exn$1;
671
693
  }
@@ -1020,7 +1042,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1020
1042
  sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1021
1043
  sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1022
1044
  ]);
1023
- let writeBatchMethod = async (batch, rawEvents, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities) => {
1045
+ let writeBatchMethod = async (batch, rawEvents, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities) => {
1024
1046
  let pgUpdates = [];
1025
1047
  let chUpdates = [];
1026
1048
  for (let i = 0, i_finish = updatedEntities.length; i < i_finish; ++i) {
@@ -1043,7 +1065,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1043
1065
  sinkPromise = undefined;
1044
1066
  }
1045
1067
  let primaryTimerRef = Hrtime.makeTimer();
1046
- await writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, undefined);
1068
+ await writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, undefined);
1047
1069
  return Prometheus.StorageWrite.increment("postgres", Hrtime.toSecondsFloat(Hrtime.timeSince(primaryTimerRef)));
1048
1070
  };
1049
1071
  let close = () => sql.end();
@@ -1103,7 +1125,11 @@ function makeStorageFromEnv(config, sqlOpt, pgSchemaOpt, isHasuraEnabledOpt) {
1103
1125
  }, pgSchema, Config.getPgUserEntities(config), Env.Hasura.aggregateEntities, Env.Hasura.responseLimit, Schema.make(Belt_Array.map(config.allEntities, e => e.table))), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking tables`))) : undefined, isHasuraEnabled ? tableNames => Stdlib_Promise.$$catch(Hasura.trackTables(Env.Hasura.graphqlEndpoint, {
1104
1126
  role: Env.Hasura.role,
1105
1127
  secret: Env.Hasura.secret
1106
- }, pgSchema, tableNames), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking new tables`))) : undefined);
1128
+ }, pgSchema, tableNames.map(tableName => ({
1129
+ tableName: tableName,
1130
+ description: undefined,
1131
+ columnDescriptions: {}
1132
+ }))), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking new tables`))) : undefined);
1107
1133
  }
1108
1134
 
1109
1135
  function makePersistenceFromConfig(config, storageOpt) {
@@ -12,11 +12,6 @@ type blockData = {
12
12
 
13
13
  external generalizeBlockDataWithTimestamp: blockDataWithTimestamp => blockData = "%identity"
14
14
 
15
- type reorgGuard = {
16
- rangeLastBlock: blockData,
17
- prevRangeLastBlock: option<blockData>,
18
- }
19
-
20
15
  type reorgDetected = {
21
16
  scannedBlock: blockData,
22
17
  receivedBlock: blockData,
@@ -94,75 +89,57 @@ let getDataByBlockNumberCopyInThreshold = ({dataByBlockNumber, maxReorgDepth}: t
94
89
  copy
95
90
  }
96
91
 
97
- /** Registers a new reorg guard, prunes unneeded data, and returns the updated state.
98
- * Resets internal state if shouldRollbackOnReorg is false (detect-only mode)
99
- */
92
+ /** Registers observed (blockNumber, blockHash) pairs from a range fetch, prunes
93
+ * unneeded data, and returns the updated state.
94
+ *
95
+ * Iterates the provided block hashes, skips entries outside the reorg threshold,
96
+ * and compares each one against the previously scanned data. Returns on the first
97
+ * mismatch as `ReorgDetected`.
98
+ *
99
+ * Resets internal state if shouldRollbackOnReorg is false (detect-only mode).
100
+ */
100
101
  let registerReorgGuard = (
101
102
  {maxReorgDepth, shouldRollbackOnReorg} as self: t,
102
- ~reorgGuard: reorgGuard,
103
+ ~blockHashes: array<blockData>,
103
104
  ~knownHeight,
104
105
  ) => {
105
106
  let dataByBlockNumberCopyInThreshold = self->getDataByBlockNumberCopyInThreshold(~knownHeight)
107
+ let thresholdBlockNumber = knownHeight - maxReorgDepth
106
108
 
107
- let {rangeLastBlock, prevRangeLastBlock} = reorgGuard
108
-
109
- let maybeReorgDetected = switch dataByBlockNumberCopyInThreshold->Utils.Dict.dangerouslyGetNonOption(
110
- rangeLastBlock.blockNumber->Int.toString,
111
- ) {
112
- | Some(scannedBlock) if scannedBlock.blockHash !== rangeLastBlock.blockHash =>
113
- Some({
114
- receivedBlock: rangeLastBlock,
115
- scannedBlock,
116
- })
117
- | _ =>
118
- switch prevRangeLastBlock {
119
- //If parentHash is None, then it's the genesis block (no reorg)
120
- //Need to check that parentHash matches because of the dynamic contracts
121
- | None => None
122
- | Some(prevRangeLastBlock) =>
123
- switch dataByBlockNumberCopyInThreshold->Utils.Dict.dangerouslyGetNonOption(
124
- prevRangeLastBlock.blockNumber->Int.toString,
125
- ) {
126
- | Some(scannedBlock) if scannedBlock.blockHash !== prevRangeLastBlock.blockHash =>
127
- Some({
128
- receivedBlock: prevRangeLastBlock,
129
- scannedBlock,
130
- })
131
- | _ => None
109
+ let maybeReorgDetected = ref(None)
110
+ let idx = ref(0)
111
+ while maybeReorgDetected.contents === None && idx.contents < blockHashes->Array.length {
112
+ let receivedBlock = blockHashes->Array.getUnsafe(idx.contents)
113
+ if receivedBlock.blockNumber >= thresholdBlockNumber {
114
+ let key = receivedBlock.blockNumber->Int.toString
115
+ // The working copy contains both previously scanned blocks AND blocks
116
+ // already written by earlier iterations of this same call, so a duplicate
117
+ // block number with a mismatching hash inside `blockHashes` itself is
118
+ // flagged as a reorg.
119
+ switch dataByBlockNumberCopyInThreshold->Utils.Dict.dangerouslyGetNonOption(key) {
120
+ | Some(scannedBlock) if scannedBlock.blockHash !== receivedBlock.blockHash =>
121
+ maybeReorgDetected := Some({receivedBlock, scannedBlock})
122
+ | _ => dataByBlockNumberCopyInThreshold->Dict.set(key, receivedBlock)
132
123
  }
133
124
  }
125
+ idx := idx.contents + 1
134
126
  }
135
127
 
136
- switch maybeReorgDetected {
128
+ switch maybeReorgDetected.contents {
137
129
  | Some(reorgDetected) => (
138
130
  shouldRollbackOnReorg
139
131
  ? self
140
132
  : make(~chainReorgCheckpoints=[], ~maxReorgDepth, ~shouldRollbackOnReorg),
141
133
  ReorgDetected(reorgDetected),
142
134
  )
143
- | None => {
144
- dataByBlockNumberCopyInThreshold->Dict.set(
145
- rangeLastBlock.blockNumber->Int.toString,
146
- rangeLastBlock,
147
- )
148
- switch prevRangeLastBlock {
149
- | None => ()
150
- | Some(prevRangeLastBlock) =>
151
- dataByBlockNumberCopyInThreshold->Dict.set(
152
- prevRangeLastBlock.blockNumber->Int.toString,
153
- prevRangeLastBlock,
154
- )
155
- }
156
-
157
- (
158
- {
159
- maxReorgDepth,
160
- dataByBlockNumber: dataByBlockNumberCopyInThreshold,
161
- shouldRollbackOnReorg,
162
- },
163
- NoReorg,
164
- )
165
- }
135
+ | None => (
136
+ {
137
+ maxReorgDepth,
138
+ dataByBlockNumber: dataByBlockNumberCopyInThreshold,
139
+ shouldRollbackOnReorg,
140
+ },
141
+ NoReorg,
142
+ )
166
143
  }
167
144
  }
168
145
 
@@ -46,47 +46,39 @@ function getDataByBlockNumberCopyInThreshold(param, knownHeight) {
46
46
  return copy;
47
47
  }
48
48
 
49
- function registerReorgGuard(self, reorgGuard, knownHeight) {
49
+ function registerReorgGuard(self, blockHashes, knownHeight) {
50
50
  let maxReorgDepth = self.maxReorgDepth;
51
51
  let shouldRollbackOnReorg = self.shouldRollbackOnReorg;
52
52
  let dataByBlockNumberCopyInThreshold = getDataByBlockNumberCopyInThreshold(self, knownHeight);
53
- let prevRangeLastBlock = reorgGuard.prevRangeLastBlock;
54
- let rangeLastBlock = reorgGuard.rangeLastBlock;
55
- let scannedBlock = dataByBlockNumberCopyInThreshold[rangeLastBlock.blockNumber.toString()];
53
+ let thresholdBlockNumber = knownHeight - maxReorgDepth | 0;
56
54
  let maybeReorgDetected;
57
- let exit = 0;
58
- if (scannedBlock !== undefined && scannedBlock.blockHash !== rangeLastBlock.blockHash) {
59
- maybeReorgDetected = {
60
- scannedBlock: scannedBlock,
61
- receivedBlock: rangeLastBlock
62
- };
63
- } else {
64
- exit = 1;
65
- }
66
- if (exit === 1) {
67
- if (prevRangeLastBlock !== undefined) {
68
- let scannedBlock$1 = dataByBlockNumberCopyInThreshold[prevRangeLastBlock.blockNumber.toString()];
69
- maybeReorgDetected = scannedBlock$1 !== undefined && scannedBlock$1.blockHash !== prevRangeLastBlock.blockHash ? ({
70
- scannedBlock: scannedBlock$1,
71
- receivedBlock: prevRangeLastBlock
72
- }) : undefined;
73
- } else {
74
- maybeReorgDetected = undefined;
55
+ let idx = 0;
56
+ while (maybeReorgDetected === undefined && idx < blockHashes.length) {
57
+ let receivedBlock = blockHashes[idx];
58
+ if (receivedBlock.blockNumber >= thresholdBlockNumber) {
59
+ let key = receivedBlock.blockNumber.toString();
60
+ let scannedBlock = dataByBlockNumberCopyInThreshold[key];
61
+ if (scannedBlock !== undefined && scannedBlock.blockHash !== receivedBlock.blockHash) {
62
+ maybeReorgDetected = {
63
+ scannedBlock: scannedBlock,
64
+ receivedBlock: receivedBlock
65
+ };
66
+ } else {
67
+ dataByBlockNumberCopyInThreshold[key] = receivedBlock;
68
+ }
75
69
  }
76
- }
77
- if (maybeReorgDetected !== undefined) {
70
+ idx = idx + 1 | 0;
71
+ };
72
+ let reorgDetected = maybeReorgDetected;
73
+ if (reorgDetected !== undefined) {
78
74
  return [
79
75
  shouldRollbackOnReorg ? self : make([], maxReorgDepth, shouldRollbackOnReorg),
80
76
  {
81
77
  TAG: "ReorgDetected",
82
- _0: maybeReorgDetected
78
+ _0: reorgDetected
83
79
  }
84
80
  ];
85
81
  } else {
86
- dataByBlockNumberCopyInThreshold[rangeLastBlock.blockNumber.toString()] = rangeLastBlock;
87
- if (prevRangeLastBlock !== undefined) {
88
- dataByBlockNumberCopyInThreshold[prevRangeLastBlock.blockNumber.toString()] = prevRangeLastBlock;
89
- }
90
82
  return [
91
83
  {
92
84
  shouldRollbackOnReorg: shouldRollbackOnReorg,
@@ -281,9 +281,27 @@ function patchConfig(config, processConfig) {
281
281
  newrecord.startBlock = startBlock;
282
282
  return newrecord;
283
283
  });
284
- let newrecord = {...config};
285
- newrecord.chainMap = newChainMap;
286
- return newrecord;
284
+ return {
285
+ name: config.name,
286
+ description: config.description,
287
+ handlers: config.handlers,
288
+ contractHandlers: config.contractHandlers,
289
+ shouldRollbackOnReorg: config.shouldRollbackOnReorg,
290
+ shouldSaveFullHistory: config.shouldSaveFullHistory,
291
+ storage: config.storage,
292
+ chainMap: newChainMap,
293
+ defaultChain: config.defaultChain,
294
+ ecosystem: config.ecosystem,
295
+ enableRawEvents: config.enableRawEvents,
296
+ maxAddrInPartition: config.maxAddrInPartition,
297
+ batchSize: config.batchSize,
298
+ lowercaseAddresses: config.lowercaseAddresses,
299
+ isDev: config.isDev,
300
+ userEntitiesByName: config.userEntitiesByName,
301
+ userEntities: config.userEntities,
302
+ allEntities: config.allEntities,
303
+ allEnums: config.allEnums
304
+ };
287
305
  }
288
306
 
289
307
  export {
package/src/Sink.res CHANGED
@@ -34,8 +34,8 @@ let makeClickHouse = (~host, ~database, ~username, ~password): t => {
34
34
  },
35
35
  writeBatch: async (~batch, ~updatedEntities) => {
36
36
  await Promise.all(
37
- updatedEntities->Belt.Array.map(({entityConfig, updates}) => {
38
- ClickHouse.setUpdatesOrThrow(client, ~cache, ~updates, ~entityConfig, ~database)
37
+ updatedEntities->Belt.Array.map(({entityConfig, changes}) => {
38
+ ClickHouse.setUpdatesOrThrow(client, ~cache, ~changes, ~entityConfig, ~database)
39
39
  }),
40
40
  )->Utils.Promise.ignoreValue
41
41
  await ClickHouse.setCheckpointsOrThrow(client, ~batch, ~database)
package/src/Sink.res.mjs CHANGED
@@ -21,7 +21,7 @@ function makeClickHouse(host, database, username, password) {
21
21
  },
22
22
  resume: checkpointId => ClickHouse.resume(client, database, checkpointId),
23
23
  writeBatch: async (batch, updatedEntities) => {
24
- await Promise.all(Belt_Array.map(updatedEntities, param => ClickHouse.setUpdatesOrThrow(client, cache, param.updates, param.entityConfig, database)));
24
+ await Promise.all(Belt_Array.map(updatedEntities, param => ClickHouse.setUpdatesOrThrow(client, cache, param.changes, param.entityConfig, database)));
25
25
  return await ClickHouse.setCheckpointsOrThrow(client, batch, database);
26
26
  }
27
27
  };
@@ -69,9 +69,14 @@ module SingleIndex = {
69
69
  operator,
70
70
  }
71
71
 
72
- // Should much hashing logic in InMemoryTable
72
+ // Lookups against the in-memory index table reconstruct this key from raw
73
+ // (fieldName, operator, serialized value) parts, so this is the single
74
+ // source of truth for the key format.
75
+ let toStringByParts = (~fieldName, ~operator: Operator.t, ~fieldValueHash) =>
76
+ `${fieldName}:${(operator :> string)}:${fieldValueHash}`
77
+
73
78
  let toString = ({fieldName, fieldValue, operator}) =>
74
- `${fieldName}:${(operator :> string)}:${fieldValue->FieldValue.toString}`
79
+ toStringByParts(~fieldName, ~operator, ~fieldValueHash=fieldValue->FieldValue.toString)
75
80
 
76
81
  let evaluate = (self: t, ~fieldName, ~fieldValue) =>
77
82
  self.fieldName === fieldName &&
@@ -101,6 +106,8 @@ module Index = {
101
106
  | Single(index) => index->SingleIndex.toString
102
107
  }
103
108
 
109
+ let toStringByParts = SingleIndex.toStringByParts
110
+
104
111
  let evaluate = (index: t, ~fieldName, ~fieldValue) =>
105
112
  switch index {
106
113
  | Single(index) => SingleIndex.evaluate(index, ~fieldName, ~fieldValue)
@@ -81,8 +81,12 @@ function make(fieldName, fieldValue, operator) {
81
81
  };
82
82
  }
83
83
 
84
+ function toStringByParts(fieldName, operator, fieldValueHash) {
85
+ return fieldName + `:` + operator + `:` + fieldValueHash;
86
+ }
87
+
84
88
  function toString$2(param) {
85
- return param.fieldName + `:` + param.operator + `:` + toString$1(param.fieldValue);
89
+ return toStringByParts(param.fieldName, param.operator, toString$1(param.fieldValue));
86
90
  }
87
91
 
88
92
  function evaluate(self, fieldName, fieldValue) {
@@ -102,6 +106,7 @@ function evaluate(self, fieldName, fieldValue) {
102
106
 
103
107
  let SingleIndex = {
104
108
  make: make,
109
+ toStringByParts: toStringByParts,
105
110
  toString: toString$2,
106
111
  evaluate: evaluate
107
112
  };
@@ -126,6 +131,7 @@ let Index = {
126
131
  makeSingle: makeSingle,
127
132
  getFieldName: getFieldName,
128
133
  toString: toString$3,
134
+ toStringByParts: toStringByParts,
129
135
  evaluate: evaluate$1
130
136
  };
131
137
 
@@ -122,7 +122,7 @@ let handleWriteBatch = (
122
122
  // checkpointId -> entityName -> entityChange
123
123
  let changesByCheckpoint: dict<dict<entityChange>> = Dict.make()
124
124
 
125
- updatedEntities->Array.forEach(({entityName, updates}) => {
125
+ updatedEntities->Array.forEach(({entityName, changes}) => {
126
126
  let entityDict = switch state.entities->Dict.get(entityName) {
127
127
  | Some(dict) => dict
128
128
  | None =>
@@ -132,68 +132,61 @@ let handleWriteBatch = (
132
132
  }
133
133
  let entityConfig = state.entityConfigs->Dict.getUnsafe(entityName)
134
134
 
135
- updates->Array.forEach(update => {
136
- // Helper to process a single change (Set or Delete)
137
- let processChange = (change: TestIndexerProxyStorage.serializableChange) => {
138
- switch change {
139
- | Set({entityId, entity, checkpointId}) =>
140
- // Parse entity immediately to store decoded values for proper comparisons
141
- // (bigint/BigDecimal need actual values, not JSON strings)
142
- let parsedEntity = entity->S.parseOrThrow(entityConfig.schema)
143
-
144
- // Update entities dict with parsed entity for load operations
145
- entityDict->Dict.set(entityId, parsedEntity)
146
-
147
- // Track change by checkpoint
148
- let checkpointKey = checkpointId->BigInt.toString
149
- let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
150
- | Some(changes) => changes
151
- | None =>
152
- let changes = Dict.make()
153
- changesByCheckpoint->Dict.set(checkpointKey, changes)
154
- changes
155
- }
156
- let entityChange = switch entityChanges->Dict.get(entityName) {
157
- | Some(change) => change
158
- | None =>
159
- let change = {sets: [], deleted: []}
160
- entityChanges->Dict.set(entityName, change)
161
- change
162
- }
163
- entityChange.sets->Array.push(parsedEntity->Utils.magic)->ignore
164
-
165
- | Delete({entityId, checkpointId}) =>
166
- // Update entities dict for load operations
167
- Dict.delete(entityDict->Obj.magic, entityId)
168
-
169
- // Track change by checkpoint
170
- let checkpointKey = checkpointId->BigInt.toString
171
- let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
172
- | Some(changes) => changes
173
- | None =>
174
- let changes = Dict.make()
175
- changesByCheckpoint->Dict.set(checkpointKey, changes)
176
- changes
177
- }
178
- let entityChange = switch entityChanges->Dict.get(entityName) {
179
- | Some(change) => change
180
- | None =>
181
- let change = {sets: [], deleted: []}
182
- entityChanges->Dict.set(entityName, change)
183
- change
184
- }
185
- entityChange.deleted->Array.push(entityId)->ignore
135
+ let processChange = (change: TestIndexerProxyStorage.serializableChange) => {
136
+ switch change {
137
+ | Set({entityId, entity, checkpointId}) =>
138
+ // Parse entity immediately to store decoded values for proper comparisons
139
+ // (bigint/BigDecimal need actual values, not JSON strings)
140
+ let parsedEntity = entity->S.parseOrThrow(entityConfig.schema)
141
+
142
+ // Update entities dict with parsed entity for load operations
143
+ entityDict->Dict.set(entityId, parsedEntity)
144
+
145
+ // Track change by checkpoint
146
+ let checkpointKey = checkpointId->BigInt.toString
147
+ let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
148
+ | Some(changes) => changes
149
+ | None =>
150
+ let changes = Dict.make()
151
+ changesByCheckpoint->Dict.set(checkpointKey, changes)
152
+ changes
153
+ }
154
+ let entityChange = switch entityChanges->Dict.get(entityName) {
155
+ | Some(change) => change
156
+ | None =>
157
+ let change = {sets: [], deleted: []}
158
+ entityChanges->Dict.set(entityName, change)
159
+ change
160
+ }
161
+ entityChange.sets->Array.push(parsedEntity->Utils.magic)->ignore
162
+
163
+ | Delete({entityId, checkpointId}) =>
164
+ // Update entities dict for load operations
165
+ Dict.delete(entityDict->Obj.magic, entityId)
166
+
167
+ // Track change by checkpoint
168
+ let checkpointKey = checkpointId->BigInt.toString
169
+ let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
170
+ | Some(changes) => changes
171
+ | None =>
172
+ let changes = Dict.make()
173
+ changesByCheckpoint->Dict.set(checkpointKey, changes)
174
+ changes
186
175
  }
176
+ let entityChange = switch entityChanges->Dict.get(entityName) {
177
+ | Some(change) => change
178
+ | None =>
179
+ let change = {sets: [], deleted: []}
180
+ entityChanges->Dict.set(entityName, change)
181
+ change
182
+ }
183
+ entityChange.deleted->Array.push(entityId)->ignore
187
184
  }
185
+ }
188
186
 
189
- // Iterate over all history entries (mirroring PgStorage.res behavior)
190
- update.history->Array.forEach(processChange)
191
-
192
- // Also include latestChange if history is empty (fallback for backwards compatibility)
193
- if update.history->Array.length === 0 {
194
- processChange(update.latestChange)
195
- }
196
- })
187
+ // Every change carries its own checkpointId and each (id, checkpointId)
188
+ // appears at most once in the batch, so record them all into their buckets.
189
+ changes->Array.forEach(processChange)
197
190
  })
198
191
 
199
192
  // Build combined checkpoint + entity changes objects