envio 3.9.0 → 3.10.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 (110) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +35 -3
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +52 -44
  10. package/src/Config.res.mjs +51 -17
  11. package/src/Core.res +4 -0
  12. package/src/CrossChainState.res +15 -23
  13. package/src/CrossChainState.res.mjs +10 -18
  14. package/src/CrossChainState.resi +4 -1
  15. package/src/Ecosystem.res +7 -2
  16. package/src/Envio.res +6 -4
  17. package/src/EventConfigBuilder.res +169 -26
  18. package/src/EventConfigBuilder.res.mjs +124 -15
  19. package/src/HandlerRegister.res +21 -4
  20. package/src/HandlerRegister.res.mjs +24 -5
  21. package/src/InMemoryStore.res +9 -7
  22. package/src/InMemoryStore.res.mjs +3 -3
  23. package/src/IndexerState.res +43 -6
  24. package/src/IndexerState.res.mjs +36 -7
  25. package/src/IndexerState.resi +4 -4
  26. package/src/Internal.res +16 -17
  27. package/src/Metrics.res +72 -6
  28. package/src/Metrics.res.mjs +30 -2
  29. package/src/Persistence.res +36 -30
  30. package/src/Persistence.res.mjs +1 -20
  31. package/src/PgStorage.res +104 -56
  32. package/src/PgStorage.res.mjs +57 -35
  33. package/src/PruneStaleHistory.res +85 -52
  34. package/src/PruneStaleHistory.res.mjs +63 -37
  35. package/src/Rollback.res +68 -45
  36. package/src/Rollback.res.mjs +38 -19
  37. package/src/RollbackCommit.res +7 -9
  38. package/src/RollbackCommit.res.mjs +5 -6
  39. package/src/SafeCheckpointTracking.res +11 -11
  40. package/src/SafeCheckpointTracking.res.mjs +7 -6
  41. package/src/SimulateItems.res +33 -18
  42. package/src/SimulateItems.res.mjs +26 -22
  43. package/src/Sink.res +38 -26
  44. package/src/Sink.res.mjs +21 -16
  45. package/src/TestIndexer.res +5 -5
  46. package/src/TestIndexer.res.mjs +1 -1
  47. package/src/Utils.res +170 -0
  48. package/src/Utils.res.mjs +153 -0
  49. package/src/Writing.res +2 -2
  50. package/src/Writing.res.mjs +1 -1
  51. package/src/bindings/ClickHouse.res +366 -698
  52. package/src/bindings/ClickHouse.res.mjs +371 -421
  53. package/src/bindings/ClickHouseSink.res +337 -0
  54. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  55. package/src/bindings/EventSource.res +8 -2
  56. package/src/bindings/NodeJs.res +6 -0
  57. package/src/bindings/Vitest.res +23 -0
  58. package/src/bindings/Vitest.res.mjs +3 -0
  59. package/src/bindings/WebSocket.res +9 -10
  60. package/src/db/CheckpointBounds.res +53 -0
  61. package/src/db/CheckpointBounds.res.mjs +44 -0
  62. package/src/db/EntityFilter.res +36 -17
  63. package/src/db/EntityFilter.res.mjs +25 -14
  64. package/src/db/EntityHistory.res +47 -22
  65. package/src/db/EntityHistory.res.mjs +19 -12
  66. package/src/db/InternalTable.res +84 -35
  67. package/src/db/InternalTable.res.mjs +56 -23
  68. package/src/db/RollbackFloors.res +55 -0
  69. package/src/db/RollbackFloors.res.mjs +92 -0
  70. package/src/db/Table.res +22 -4
  71. package/src/db/Table.res.mjs +30 -9
  72. package/src/sources/Evm.res +2 -0
  73. package/src/sources/Evm.res.mjs +2 -0
  74. package/src/sources/EvmHyperSyncSource.res +3 -3
  75. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  76. package/src/sources/EvmRpcWs.res +124 -0
  77. package/src/sources/EvmRpcWs.res.mjs +117 -0
  78. package/src/sources/Fuel.res +2 -0
  79. package/src/sources/Fuel.res.mjs +2 -0
  80. package/src/sources/FuelHyperSyncSource.res +1 -1
  81. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  82. package/src/sources/HeightFeed.res +568 -0
  83. package/src/sources/HeightFeed.res.mjs +452 -0
  84. package/src/sources/HeightStream.res +257 -0
  85. package/src/sources/HeightStream.res.mjs +194 -0
  86. package/src/sources/HyperSync.res +5 -0
  87. package/src/sources/HyperSync.res.mjs +3 -0
  88. package/src/sources/HyperSync.resi +4 -0
  89. package/src/sources/HyperSyncSSE.res +49 -0
  90. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  91. package/src/sources/RpcSource.res +1 -1
  92. package/src/sources/RpcSource.res.mjs +2 -2
  93. package/src/sources/Source.res +54 -1
  94. package/src/sources/Source.res.mjs +25 -0
  95. package/src/sources/SourceManager.res +220 -227
  96. package/src/sources/SourceManager.res.mjs +139 -164
  97. package/src/sources/SourceManager.resi +8 -0
  98. package/src/sources/Svm.res +2 -0
  99. package/src/sources/Svm.res.mjs +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +70 -127
  101. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  102. package/src/sources/SvmHyperSyncSource.res +18 -15
  103. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  104. package/svm.schema.json +1 -75
  105. package/src/MemoryStorage.res +0 -729
  106. package/src/MemoryStorage.res.mjs +0 -586
  107. package/src/sources/HyperSyncHeightStream.res +0 -129
  108. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  109. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  110. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -29,6 +29,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
29
29
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
30
30
  import * as ContractMapping from "./ContractMapping.res.mjs";
31
31
  import * as IndexDefinition from "./db/IndexDefinition.res.mjs";
32
+ import * as CheckpointBounds from "./db/CheckpointBounds.res.mjs";
32
33
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
33
34
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
34
35
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
@@ -207,7 +208,7 @@ function getEntityHistory(entityConfig) {
207
208
  checkpointIdField,
208
209
  actionField
209
210
  ]), undefined);
210
- let setChangeSchema = EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), getRowSchema(entityConfig));
211
+ let setChangeSchema = EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), undefined, getRowSchema(entityConfig));
211
212
  let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema);
212
213
  let cache$1 = {
213
214
  table: table,
@@ -296,7 +297,7 @@ function makeFilterCondition(filter, table, params) {
296
297
  let serializeParamOrThrow = (queryField, fieldName, fieldValue, isArray) => {
297
298
  let param;
298
299
  try {
299
- param = S$RescriptSchema.reverseConvertToJsonOrThrow(fieldValue, isArray ? queryField.arrayFieldSchema : queryField.fieldSchema);
300
+ param = S$RescriptSchema.reverseConvertOrThrow(fieldValue, isArray ? queryField.arrayFieldSchema : queryField.fieldSchema);
300
301
  } catch (raw_exn) {
301
302
  let exn = Primitive_exceptions.internalToException(raw_exn);
302
303
  throw {
@@ -410,12 +411,20 @@ VALUES` + placeholders + (
410
411
  function makeTableBatchSetQuery(pgSchema, table, itemSchema, chainIdModeOpt) {
411
412
  let chainIdMode = chainIdModeOpt !== undefined ? chainIdModeOpt : "int32";
412
413
  let match = Table.toSqlParams(table, itemSchema, pgSchema, chainIdMode);
414
+ let byteaColumnIndexes = match.byteaColumnIndexes;
413
415
  let isRawEvents = table.tableName === InternalTable.RawEvents.table.tableName;
414
416
  let isHistoryUpdate = table.tableName.startsWith(EntityHistory.historyTablePrefix);
415
417
  if ((isRawEvents || !match.hasArrayField) && !isHistoryUpdate) {
416
418
  return {
417
419
  query: makeInsertUnnestSetQuery(pgSchema, table, itemSchema, isRawEvents, chainIdMode),
418
- convertOrThrow: S$RescriptSchema.compile(S$RescriptSchema.unnest(match.dbSchema), "Output", "Input", "Sync", false),
420
+ convertOrThrow: S$RescriptSchema.compile(S$RescriptSchema.preprocess(S$RescriptSchema.unnest(match.dbSchema), param => ({
421
+ s: columns => {
422
+ byteaColumnIndexes.forEach(index => {
423
+ columns[index] = Utils.Bytes.toPgArrayLiteral(columns[index]);
424
+ });
425
+ return columns;
426
+ }
427
+ })), "Output", "Input", "Sync", false),
419
428
  isInsertValues: false
420
429
  };
421
430
  } else {
@@ -681,7 +690,7 @@ function executeSet(sql, items, dbFunction) {
681
690
  async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, setQueryCache, updatedEffectsCache, updatedEntities, registeredAddresses, sinkPromise, chainMetaData, escapeTables) {
682
691
  try {
683
692
  let chainIdMode = config.chainIdMode;
684
- let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold);
693
+ let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold, undefined);
685
694
  let specificError = {
686
695
  contents: undefined
687
696
  };
@@ -723,6 +732,7 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
723
732
  let idsToDelete = [];
724
733
  let scopeChainId;
725
734
  scopeChainId = scope === "crossChain" ? undefined : Primitive_option.some(scope);
735
+ let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold, scopeChainId);
726
736
  let match = Table.getChainIdField(entityConfig.table);
727
737
  let changes$1;
728
738
  if (match !== undefined && scopeChainId !== undefined) {
@@ -835,11 +845,15 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
835
845
  });
836
846
  let rollbackTables;
837
847
  if (rollback !== undefined) {
848
+ let progressedChains = rollback.progressedChains;
838
849
  let rolledBackAddresses = rollback.rolledBackAddresses;
839
- let rollbackTargetCheckpointId = rollback.targetCheckpointId;
850
+ let floors = rollback.floors;
840
851
  rollbackTables = sql => {
841
- let promises = allEntities.filter(entityConfig => entityConfig.storage.postgres).map(entityConfig => EntityHistory.rollback(sql, pgSchema, entityConfig.name, entityConfig.index, rollbackTargetCheckpointId));
842
- promises.push(InternalTable.Checkpoints.rollback(sql, pgSchema, rollbackTargetCheckpointId));
852
+ let promises = allEntities.filter(entityConfig => entityConfig.storage.postgres).map(entityConfig => EntityHistory.rollback(sql, pgSchema, entityConfig.name, entityConfig.index, Table.getPgChainIdColumn(entityConfig.table), floors));
853
+ promises.push(InternalTable.Checkpoints.rollback(sql, pgSchema, floors));
854
+ if (Utils.$$Array.notEmpty(progressedChains)) {
855
+ promises.push(InternalTable.Chains.setProgressedChains(sql, pgSchema, progressedChains));
856
+ }
843
857
  if (Utils.$$Array.notEmpty(rolledBackAddresses)) {
844
858
  promises.push(InternalTable.EnvioAddresses.$$delete(sql, pgSchema, rolledBackAddresses, chainIdMode));
845
859
  }
@@ -917,41 +931,45 @@ function rollbackKeyColumns(entityConfig) {
917
931
  }
918
932
  }
919
933
 
920
- function makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema) {
934
+ function makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema, floors) {
921
935
  let dataFieldNames = Stdlib_Array.filterMap(entityConfig.table.fields, fieldOrDerived => {
922
936
  if (fieldOrDerived.TAG === "Field") {
923
937
  return Table.getPgDbFieldName(fieldOrDerived._0);
924
938
  }
925
939
  });
926
- let dataFieldsCommaSeparated = dataFieldNames.map(name => `"` + name + `"`).join(", ");
927
940
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
941
+ let tableRef = `"` + historyTableName + `"`;
942
+ let dataFieldsCommaSeparated = dataFieldNames.map(name => tableRef + `."` + name + `"`).join(", ");
928
943
  let keyColumns = rollbackKeyColumns(entityConfig);
929
- let keyColumnsCommaSeparated = keyColumns.map(c => `"` + c + `"`).join(", ");
930
- let keyMatch = keyColumns.map(c => `h."` + c + `" = "` + historyTableName + `"."` + c + `"`).join(" AND ");
931
- return `SELECT DISTINCT ON (` + keyColumnsCommaSeparated + `) ` + dataFieldsCommaSeparated + `, "` + EntityHistory.changeFieldName + `"
932
- FROM "` + pgSchema + `"."` + historyTableName + `"
933
- WHERE "` + EntityHistory.checkpointIdFieldName + `" <= $1
944
+ let keyColumnsCommaSeparated = keyColumns.map(c => tableRef + `."` + c + `"`).join(", ");
945
+ let keyMatch = keyColumns.map(c => `h."` + c + `" = ` + tableRef + `."` + c + `"`).join(" AND ");
946
+ let bounds = CheckpointBounds.sql(floors.floors, Table.getPgChainIdColumn(entityConfig.table), tableRef);
947
+ return `SELECT DISTINCT ON (` + keyColumnsCommaSeparated + `) ` + dataFieldsCommaSeparated + `, ` + tableRef + `."` + EntityHistory.changeFieldName + `"
948
+ FROM "` + pgSchema + `"."` + historyTableName + `"` + bounds.join + `
949
+ WHERE ` + tableRef + `."` + EntityHistory.checkpointIdFieldName + `" <= ` + bounds.checkpointId + `
934
950
  AND EXISTS (
935
951
  SELECT 1
936
952
  FROM "` + pgSchema + `"."` + historyTableName + `" h
937
953
  WHERE ` + keyMatch + `
938
- AND h."` + EntityHistory.checkpointIdFieldName + `" > $1
954
+ AND h."` + EntityHistory.checkpointIdFieldName + `" > ` + bounds.checkpointId + `
939
955
  )
940
- ORDER BY ` + keyColumnsCommaSeparated + `, "` + EntityHistory.checkpointIdFieldName + `" DESC`;
956
+ ORDER BY ` + keyColumnsCommaSeparated + `, ` + tableRef + `."` + EntityHistory.checkpointIdFieldName + `" DESC`;
941
957
  }
942
958
 
943
- function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) {
959
+ function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema, floors) {
944
960
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
961
+ let tableRef = `"` + historyTableName + `"`;
945
962
  let keyColumns = rollbackKeyColumns(entityConfig);
946
- let keyMatch = keyColumns.map(c => `h."` + c + `" = "` + historyTableName + `"."` + c + `"`).join(" AND ");
947
- return `SELECT DISTINCT ` + keyColumns.map(c => `"` + c + `"`).join(", ") + `
948
- FROM "` + pgSchema + `"."` + historyTableName + `"
949
- WHERE "` + EntityHistory.checkpointIdFieldName + `" > $1
963
+ let keyMatch = keyColumns.map(c => `h."` + c + `" = ` + tableRef + `."` + c + `"`).join(" AND ");
964
+ let bounds = CheckpointBounds.sql(floors.floors, Table.getPgChainIdColumn(entityConfig.table), tableRef);
965
+ return `SELECT DISTINCT ` + keyColumns.map(c => tableRef + `."` + c + `"`).join(", ") + `
966
+ FROM "` + pgSchema + `"."` + historyTableName + `"` + bounds.join + `
967
+ WHERE ` + tableRef + `."` + EntityHistory.checkpointIdFieldName + `" > ` + bounds.checkpointId + `
950
968
  AND NOT EXISTS (
951
969
  SELECT 1
952
970
  FROM "` + pgSchema + `"."` + historyTableName + `" h
953
971
  WHERE ` + keyMatch + `
954
- AND h."` + EntityHistory.checkpointIdFieldName + `" <= $1
972
+ AND h."` + EntityHistory.checkpointIdFieldName + `" <= ` + bounds.checkpointId + `
955
973
  )`;
956
974
  }
957
975
 
@@ -1113,13 +1131,12 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1113
1131
  let entities = entitiesOpt !== undefined ? entitiesOpt : [];
1114
1132
  let enums = enumsOpt !== undefined ? enumsOpt : [];
1115
1133
  let pgEntities = entities.filter(e => e.storage.postgres);
1116
- let chEntities = entities.filter(e => e.storage.clickhouse);
1117
1134
  let schemaTableNames = await sql.unsafe(makeSchemaTableNamesQuery(pgSchema));
1118
1135
  if (Utils.$$Array.notEmpty(schemaTableNames) && !schemaTableNames.some(table => table.table_name === InternalTable.Chains.table.tableName ? true : table.table_name === "event_sync_state")) {
1119
1136
  Stdlib_JsError.throwWithMessage(`Cannot run Envio migrations on PostgreSQL schema "` + pgSchema + `" because it contains non-Envio tables. Running migrations would delete all data in this schema.\n\nTo resolve this:\n1. If you want to use this schema, first backup any important data, then drop it with: "pnpm envio local db-migrate down"\n2. Or specify a different schema name by setting the "ENVIO_PG_SCHEMA" environment variable\n3. Or manually drop the schema in your database if you're certain the data is not needed.`);
1120
1137
  }
1121
1138
  if (sink !== undefined) {
1122
- await sink.initialize(chainConfigs, chEntities, enums);
1139
+ await sink.initialize(entities);
1123
1140
  }
1124
1141
  let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames), true, chainIdMode);
1125
1142
  let rowsByChain = chainConfigs.map(chainConfig => ChainState.configStorageRows(chainConfig, ecosystem, contractMapping));
@@ -1398,7 +1415,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1398
1415
  return Logging.errorWithExn(Utils.prettifyExn(exn$1), `Failed to dump cache.`);
1399
1416
  }
1400
1417
  };
1401
- let resumeInitialState = async () => {
1418
+ let resumeInitialState = async (entities, throwIfIncompatible) => {
1402
1419
  let match = await Promise.all([
1403
1420
  restoreEffectCache(false),
1404
1421
  InternalTable.Chains.getInitialState(sql, pgSchema).then(rawInitialStates => rawInitialStates.map(rawInitialState => ({
@@ -1435,6 +1452,10 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1435
1452
  })
1436
1453
  ]);
1437
1454
  let match$1 = match[4];
1455
+ let storedContractMapping = match$1[1];
1456
+ let storedEnvioInfo = match$1[0];
1457
+ let chains = match[1];
1458
+ throwIfIncompatible(storedEnvioInfo, storedContractMapping);
1438
1459
  await reloadIndexCatalog();
1439
1460
  let checkpointId = BigInt(match[2][0].id);
1440
1461
  let reorgCheckpoints = match[3].map(raw => ({
@@ -1444,14 +1465,14 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1444
1465
  block_hash: raw.block_hash
1445
1466
  }));
1446
1467
  if (sink !== undefined) {
1447
- await sink.resume(checkpointId);
1468
+ await sink.resume(checkpointId, chains, entities);
1448
1469
  }
1449
1470
  return {
1450
1471
  cleanRun: false,
1451
- contractMapping: match$1[1],
1452
- envioInfo: match$1[0],
1472
+ contractMapping: storedContractMapping,
1473
+ envioInfo: storedEnvioInfo,
1453
1474
  cache: match[0],
1454
- chains: match[1],
1475
+ chains: chains,
1455
1476
  checkpointId: checkpointId,
1456
1477
  reorgCheckpoints: reorgCheckpoints
1457
1478
  };
@@ -1461,14 +1482,15 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1461
1482
  return await sql.unsafe(query);
1462
1483
  };
1463
1484
  let setChainMeta = chainsData => InternalTable.Chains.setMeta(sql, pgSchema, chainsData).then(param => (undefined));
1464
- let pruneStaleCheckpoints = safeCheckpointId => InternalTable.Checkpoints.pruneStaleCheckpoints(sql, pgSchema, safeCheckpointId);
1465
- let pruneStaleEntityHistory = (entityName, entityIndex, chainIdColumn, safeCheckpointId) => EntityHistory.pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, chainIdColumn, safeCheckpointId);
1485
+ let pruneStaleCheckpoints = safeCheckpoints => InternalTable.Checkpoints.pruneStaleCheckpoints(sql, pgSchema, safeCheckpoints);
1486
+ let pruneStaleEntityHistory = (entityName, entityIndex, chainIdColumn, safeCheckpoints) => EntityHistory.pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, chainIdColumn, safeCheckpoints);
1466
1487
  let getRollbackTargetCheckpoint = (reorgChainId, lastKnownValidBlockNumber) => InternalTable.Checkpoints.getRollbackTargetCheckpoint(sql, pgSchema, reorgChainId, lastKnownValidBlockNumber);
1467
- let getRollbackProgressDiff = rollbackTargetCheckpointId => InternalTable.Checkpoints.getRollbackProgressDiff(sql, pgSchema, rollbackTargetCheckpointId);
1468
- let getRollbackData = async (entityConfig, rollbackTargetCheckpointId) => {
1488
+ let getRollbackProgressDiff = floors => InternalTable.Checkpoints.getRollbackProgressDiff(sql, pgSchema, floors);
1489
+ let getRollbackData = async (entityConfig, floors) => {
1490
+ let params = CheckpointBounds.params(floors.floors);
1469
1491
  let match = await Promise.all([
1470
- sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1471
- sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1492
+ sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema, floors), params, {prepare: true}),
1493
+ sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema, floors), params, {prepare: true})
1472
1494
  ]);
1473
1495
  let chainIdSchema = rollbackChainIdSchema(entityConfig.table);
1474
1496
  let scopeOf = row => {
@@ -12,11 +12,43 @@ let maxEntitiesPerWrite = 5
12
12
  let forcedIntervalMultiplier = 5.
13
13
 
14
14
  type targets = {
15
- safeCheckpointId: Internal.checkpointId,
15
+ safeCheckpoints: CheckpointBounds.t,
16
16
  concurrent: array<Internal.entityConfig>,
17
17
  forced: array<Internal.entityConfig>,
18
18
  }
19
19
 
20
+ // Where each chain's history may be pruned back to, or None while nowhere can.
21
+ //
22
+ // A rollback only ever deletes a chain's rows down to that chain's own floor,
23
+ // which is set by its own reorg and so never reaches below its own safe
24
+ // checkpoint. Each chain can therefore prune to its own, and a chain with
25
+ // nothing safe yet holds only itself back.
26
+ //
27
+ // Unless the schema has a cross-chain entity: a reorg on any chain then rolls
28
+ // every chain back to one checkpoint, which can sit below another chain's safe
29
+ // point, so the lowest is the only bound every chain agrees on.
30
+ let selectSafeCheckpoints = (state: IndexerState.t) => {
31
+ let byChain = state->IndexerState.getSafeCheckpointIdByChain
32
+ let safe =
33
+ byChain->Array.filterMap(((chainId, checkpointId)) =>
34
+ checkpointId->Option.map(checkpointId => (chainId, checkpointId))
35
+ )
36
+ if state->IndexerState.config->Config.isIsolatedMultichain {
37
+ safe->Utils.Array.notEmpty ? Some(CheckpointBounds.PerChain(safe)) : None
38
+ } else if safe->Array.length === byChain->Array.length {
39
+ let (_, lowest) = safe->Array.getUnsafe(0)
40
+ Some(
41
+ CheckpointBounds.EveryChain(
42
+ safe->Array.reduce(lowest, (lowest, (_, checkpointId)) =>
43
+ Pervasives.min(lowest, checkpointId)
44
+ ),
45
+ ),
46
+ )
47
+ } else {
48
+ None
49
+ }
50
+ }
51
+
20
52
  let selectFrom = (
21
53
  ~allEntities: array<Internal.entityConfig>,
22
54
  ~lastPrunedAtMillis: dict<float>,
@@ -24,7 +56,7 @@ let selectFrom = (
24
56
  ~isRollback,
25
57
  ~nowMillis,
26
58
  ~intervalMillis,
27
- ~safeCheckpointId,
59
+ ~safeCheckpoints,
28
60
  ) => {
29
61
  let byOldestPrune = ((a, _), (b, _)) => a -. b
30
62
  let toEntities = candidates => candidates->Array.map(((_, entityConfig)) => entityConfig)
@@ -60,7 +92,7 @@ let selectFrom = (
60
92
  }
61
93
 
62
94
  {
63
- safeCheckpointId,
95
+ safeCheckpoints,
64
96
  concurrent: sortedConcurrent->Array.slice(~start=0, ~end=maxEntitiesPerWrite)->toEntities,
65
97
  forced: forcedCandidates
66
98
  ->Array.toSorted(byOldestPrune)
@@ -72,62 +104,63 @@ let selectFrom = (
72
104
  let select = (state: IndexerState.t, ~writtenEntityNames, ~isRollback) => {
73
105
  let config = state->IndexerState.config
74
106
  if config->Config.shouldPruneHistory(~isInReorgThreshold=state->IndexerState.isInReorgThreshold) {
75
- switch state->IndexerState.getSafeCheckpointId {
76
- | None => None
77
- | Some(safeCheckpointId) =>
78
- Some(
79
- selectFrom(
80
- ~allEntities=(state->IndexerState.persistence).allEntities,
81
- ~lastPrunedAtMillis=state->IndexerState.lastPrunedAtMillis,
82
- ~writtenEntityNames,
83
- ~isRollback,
84
- ~nowMillis=Date.now(),
85
- ~intervalMillis=Env.ThrottleWrites.pruneStaleDataIntervalMillis->Int.toFloat,
86
- ~safeCheckpointId,
87
- ),
107
+ state
108
+ ->selectSafeCheckpoints
109
+ ->Option.map(safeCheckpoints =>
110
+ selectFrom(
111
+ ~allEntities=(state->IndexerState.persistence).allEntities,
112
+ ~lastPrunedAtMillis=state->IndexerState.lastPrunedAtMillis,
113
+ ~writtenEntityNames,
114
+ ~isRollback,
115
+ ~nowMillis=Date.now(),
116
+ ~intervalMillis=Env.ThrottleWrites.pruneStaleDataIntervalMillis->Int.toFloat,
117
+ ~safeCheckpoints,
88
118
  )
89
- }
119
+ )
90
120
  } else {
91
121
  None
92
122
  }
93
123
  }
94
124
 
95
- let pruneEntities = async (state: IndexerState.t, ~entities, ~safeCheckpointId) => {
125
+ let pruneEntity = async (
126
+ state: IndexerState.t,
127
+ ~entityConfig: Internal.entityConfig,
128
+ ~safeCheckpoints,
129
+ ) => {
96
130
  let persistence = state->IndexerState.persistence
97
- for idx in 0 to entities->Array.length - 1 {
98
- let entityConfig: Internal.entityConfig = entities->Array.getUnsafe(idx)
99
- let timeRef = Performance.now()
100
- // Recorded for failures too, so a failing prune retries on the same
101
- // interval instead of on every write.
102
- state->IndexerState.lastPrunedAtMillis->Dict.set(entityConfig.name, Date.now())
103
- switch await persistence.storage.pruneStaleEntityHistory(
131
+ let timeRef = Performance.now()
132
+ // Recorded for failures too, so a failing prune retries on the same
133
+ // interval instead of on every write.
134
+ state->IndexerState.lastPrunedAtMillis->Dict.set(entityConfig.name, Date.now())
135
+ switch await persistence.storage.pruneStaleEntityHistory(
136
+ ~entityName=entityConfig.name,
137
+ ~entityIndex=entityConfig.index,
138
+ ~chainIdColumn=entityConfig.table->Table.getPgChainIdColumn,
139
+ ~safeCheckpoints,
140
+ ) {
141
+ | () =>
142
+ state->IndexerState.recordHistoryPrune(
143
+ ~timeSeconds=Performance.secondsSince(timeRef),
104
144
  ~entityName=entityConfig.name,
105
- ~entityIndex=entityConfig.index,
106
- ~chainIdColumn=entityConfig.table->Table.getChainIdField->Option.map(Table.getPgDbFieldName),
107
- ~safeCheckpointId,
108
- ) {
109
- | () =>
110
- state->IndexerState.recordHistoryPrune(
111
- ~timeSeconds=Performance.secondsSince(timeRef),
112
- ~entityName=entityConfig.name,
113
- )
114
- | exception exn =>
115
- // Pruning is cleanup; a failure must not fail the write loop.
116
- Logging.createChild(
117
- ~params={
118
- "entityName": entityConfig.name,
119
- "safeCheckpointId": safeCheckpointId,
120
- },
121
- )->Logging.childErrorWithExn(exn->Utils.prettifyExn, `Failed to prune stale entity history`)
122
- }
145
+ )
146
+ | exception exn =>
147
+ // Pruning is cleanup; a failure must not fail the write loop.
148
+ Logging.createChild(
149
+ ~params={"entityName": entityConfig.name, "safeCheckpoints": safeCheckpoints},
150
+ )->Logging.childErrorWithExn(exn->Utils.prettifyExn, `Failed to prune stale entity history`)
123
151
  }
124
152
  }
125
153
 
126
- let pruneCheckpoints = async (state: IndexerState.t, ~safeCheckpointId) => {
127
- switch await (state->IndexerState.persistence).storage.pruneStaleCheckpoints(~safeCheckpointId) {
154
+ let pruneEntities = (state: IndexerState.t, ~entities, ~safeCheckpoints) =>
155
+ entities->Utils.Array.awaitEach(entityConfig =>
156
+ state->pruneEntity(~entityConfig, ~safeCheckpoints)
157
+ )
158
+
159
+ let pruneCheckpoints = async (state: IndexerState.t, ~safeCheckpoints) => {
160
+ switch await (state->IndexerState.persistence).storage.pruneStaleCheckpoints(~safeCheckpoints) {
128
161
  | () => ()
129
162
  | exception exn =>
130
- Logging.createChild(~params={"safeCheckpointId": safeCheckpointId})->Logging.childErrorWithExn(
163
+ Logging.createChild(~params={"safeCheckpoints": safeCheckpoints})->Logging.childErrorWithExn(
131
164
  exn->Utils.prettifyExn,
132
165
  `Failed to prune stale checkpoints`,
133
166
  )
@@ -136,22 +169,22 @@ let pruneCheckpoints = async (state: IndexerState.t, ~safeCheckpointId) => {
136
169
 
137
170
  let runConcurrent = async (state: IndexerState.t, ~targets) => {
138
171
  switch targets {
139
- | Some({safeCheckpointId, concurrent}) if concurrent->Utils.Array.notEmpty =>
140
- await pruneCheckpoints(state, ~safeCheckpointId)
141
- await pruneEntities(state, ~entities=concurrent, ~safeCheckpointId)
172
+ | Some({safeCheckpoints, concurrent}) if concurrent->Utils.Array.notEmpty =>
173
+ await pruneCheckpoints(state, ~safeCheckpoints)
174
+ await pruneEntities(state, ~entities=concurrent, ~safeCheckpoints)
142
175
  | Some(_) | None => ()
143
176
  }
144
177
  }
145
178
 
146
179
  let runForced = async (state: IndexerState.t, ~targets) => {
147
180
  switch targets {
148
- | Some({safeCheckpointId, concurrent, forced}) if forced->Utils.Array.notEmpty =>
181
+ | Some({safeCheckpoints, concurrent, forced}) if forced->Utils.Array.notEmpty =>
149
182
  // When nothing ran concurrently (eg a rollback write), checkpoint pruning
150
183
  // lands here, after the write, so it never overlaps a rollback transaction.
151
184
  if concurrent->Utils.Array.isEmpty {
152
- await pruneCheckpoints(state, ~safeCheckpointId)
185
+ await pruneCheckpoints(state, ~safeCheckpoints)
153
186
  }
154
- await pruneEntities(state, ~entities=forced, ~safeCheckpointId)
187
+ await pruneEntities(state, ~entities=forced, ~safeCheckpoints)
155
188
  | Some(_) | None => ()
156
189
  }
157
190
  }
@@ -7,10 +7,41 @@ import * as Config from "./Config.res.mjs";
7
7
  import * as Logging from "./Logging.res.mjs";
8
8
  import * as Performance from "./bindings/Performance.res.mjs";
9
9
  import * as IndexerState from "./IndexerState.res.mjs";
10
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
10
11
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
12
+ import * as Primitive_bigint from "@rescript/runtime/lib/es6/Primitive_bigint.js";
11
13
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
14
 
13
- function selectFrom(allEntities, lastPrunedAtMillis, writtenEntityNames, isRollback, nowMillis, intervalMillis, safeCheckpointId) {
15
+ function selectSafeCheckpoints(state) {
16
+ let byChain = IndexerState.getSafeCheckpointIdByChain(state);
17
+ let safe = Stdlib_Array.filterMap(byChain, param => {
18
+ let chainId = param[0];
19
+ return Stdlib_Option.map(param[1], checkpointId => [
20
+ chainId,
21
+ checkpointId
22
+ ]);
23
+ });
24
+ if (Config.isIsolatedMultichain(IndexerState.config(state))) {
25
+ if (Utils.$$Array.notEmpty(safe)) {
26
+ return {
27
+ TAG: "PerChain",
28
+ _0: safe
29
+ };
30
+ } else {
31
+ return;
32
+ }
33
+ }
34
+ if (safe.length !== byChain.length) {
35
+ return;
36
+ }
37
+ let match = safe[0];
38
+ return {
39
+ TAG: "EveryChain",
40
+ _0: Stdlib_Array.reduce(safe, match[1], (lowest, param) => Primitive_bigint.min(lowest, param[1]))
41
+ };
42
+ }
43
+
44
+ function selectFrom(allEntities, lastPrunedAtMillis, writtenEntityNames, isRollback, nowMillis, intervalMillis, safeCheckpoints) {
14
45
  let byOldestPrune = (param, param$1) => param[0] - param$1[0];
15
46
  let toEntities = candidates => candidates.map(param => param[1]);
16
47
  let concurrentCandidates = [];
@@ -44,7 +75,7 @@ function selectFrom(allEntities, lastPrunedAtMillis, writtenEntityNames, isRollb
44
75
  }
45
76
  }
46
77
  return {
47
- safeCheckpointId: safeCheckpointId,
78
+ safeCheckpoints: safeCheckpoints,
48
79
  concurrent: toEntities(sortedConcurrent.slice(0, 5)),
49
80
  forced: toEntities(forcedCandidates.toSorted(byOldestPrune).slice(0, 5))
50
81
  };
@@ -52,47 +83,40 @@ function selectFrom(allEntities, lastPrunedAtMillis, writtenEntityNames, isRollb
52
83
 
53
84
  function select(state, writtenEntityNames, isRollback) {
54
85
  let config = IndexerState.config(state);
55
- if (!Config.shouldPruneHistory(config, IndexerState.isInReorgThreshold(state))) {
56
- return;
57
- }
58
- let safeCheckpointId = IndexerState.getSafeCheckpointId(state);
59
- if (safeCheckpointId !== undefined) {
60
- return selectFrom(IndexerState.persistence(state).allEntities, IndexerState.lastPrunedAtMillis(state), writtenEntityNames, isRollback, Date.now(), Env.ThrottleWrites.pruneStaleDataIntervalMillis, safeCheckpointId);
86
+ if (Config.shouldPruneHistory(config, IndexerState.isInReorgThreshold(state))) {
87
+ return Stdlib_Option.map(selectSafeCheckpoints(state), safeCheckpoints => selectFrom(IndexerState.persistence(state).allEntities, IndexerState.lastPrunedAtMillis(state), writtenEntityNames, isRollback, Date.now(), Env.ThrottleWrites.pruneStaleDataIntervalMillis, safeCheckpoints));
61
88
  }
62
89
  }
63
90
 
64
- async function pruneEntities(state, entities, safeCheckpointId) {
91
+ async function pruneEntity(state, entityConfig, safeCheckpoints) {
65
92
  let persistence = IndexerState.persistence(state);
66
- for (let idx = 0, idx_finish = entities.length; idx < idx_finish; ++idx) {
67
- let entityConfig = entities[idx];
68
- let timeRef = Performance.now();
69
- IndexerState.lastPrunedAtMillis(state)[entityConfig.name] = Date.now();
70
- let exit = 0;
71
- let val;
72
- try {
73
- val = await persistence.storage.pruneStaleEntityHistory(entityConfig.name, entityConfig.index, Stdlib_Option.map(Table.getChainIdField(entityConfig.table), Table.getPgDbFieldName), safeCheckpointId);
74
- exit = 1;
75
- } catch (raw_exn) {
76
- let exn = Primitive_exceptions.internalToException(raw_exn);
77
- Logging.childErrorWithExn(Logging.createChild({
78
- entityName: entityConfig.name,
79
- safeCheckpointId: safeCheckpointId
80
- }), Utils.prettifyExn(exn), `Failed to prune stale entity history`);
81
- }
82
- if (exit === 1) {
83
- IndexerState.recordHistoryPrune(state, entityConfig.name, Performance.secondsSince(timeRef));
84
- }
93
+ let timeRef = Performance.now();
94
+ IndexerState.lastPrunedAtMillis(state)[entityConfig.name] = Date.now();
95
+ let val;
96
+ try {
97
+ val = await persistence.storage.pruneStaleEntityHistory(entityConfig.name, entityConfig.index, Table.getPgChainIdColumn(entityConfig.table), safeCheckpoints);
98
+ } catch (raw_exn) {
99
+ let exn = Primitive_exceptions.internalToException(raw_exn);
100
+ return Logging.childErrorWithExn(Logging.createChild({
101
+ entityName: entityConfig.name,
102
+ safeCheckpoints: safeCheckpoints
103
+ }), Utils.prettifyExn(exn), `Failed to prune stale entity history`);
85
104
  }
105
+ return IndexerState.recordHistoryPrune(state, entityConfig.name, Performance.secondsSince(timeRef));
106
+ }
107
+
108
+ function pruneEntities(state, entities, safeCheckpoints) {
109
+ return Utils.$$Array.awaitEach(entities, entityConfig => pruneEntity(state, entityConfig, safeCheckpoints));
86
110
  }
87
111
 
88
- async function pruneCheckpoints(state, safeCheckpointId) {
112
+ async function pruneCheckpoints(state, safeCheckpoints) {
89
113
  try {
90
- await IndexerState.persistence(state).storage.pruneStaleCheckpoints(safeCheckpointId);
114
+ await IndexerState.persistence(state).storage.pruneStaleCheckpoints(safeCheckpoints);
91
115
  return;
92
116
  } catch (raw_exn) {
93
117
  let exn = Primitive_exceptions.internalToException(raw_exn);
94
118
  return Logging.childErrorWithExn(Logging.createChild({
95
- safeCheckpointId: safeCheckpointId
119
+ safeCheckpoints: safeCheckpoints
96
120
  }), Utils.prettifyExn(exn), `Failed to prune stale checkpoints`);
97
121
  }
98
122
  }
@@ -102,10 +126,10 @@ async function runConcurrent(state, targets) {
102
126
  return;
103
127
  }
104
128
  let concurrent = targets.concurrent;
105
- let safeCheckpointId = targets.safeCheckpointId;
129
+ let safeCheckpoints = targets.safeCheckpoints;
106
130
  if (Utils.$$Array.notEmpty(concurrent)) {
107
- await pruneCheckpoints(state, safeCheckpointId);
108
- return await pruneEntities(state, concurrent, safeCheckpointId);
131
+ await pruneCheckpoints(state, safeCheckpoints);
132
+ return await pruneEntities(state, concurrent, safeCheckpoints);
109
133
  }
110
134
  }
111
135
 
@@ -114,12 +138,12 @@ async function runForced(state, targets) {
114
138
  return;
115
139
  }
116
140
  let forced = targets.forced;
117
- let safeCheckpointId = targets.safeCheckpointId;
141
+ let safeCheckpoints = targets.safeCheckpoints;
118
142
  if (Utils.$$Array.notEmpty(forced)) {
119
143
  if (Utils.$$Array.isEmpty(targets.concurrent)) {
120
- await pruneCheckpoints(state, safeCheckpointId);
144
+ await pruneCheckpoints(state, safeCheckpoints);
121
145
  }
122
- return await pruneEntities(state, forced, safeCheckpointId);
146
+ return await pruneEntities(state, forced, safeCheckpoints);
123
147
  }
124
148
  }
125
149
 
@@ -130,8 +154,10 @@ let forcedIntervalMultiplier = 5;
130
154
  export {
131
155
  maxEntitiesPerWrite,
132
156
  forcedIntervalMultiplier,
157
+ selectSafeCheckpoints,
133
158
  selectFrom,
134
159
  select,
160
+ pruneEntity,
135
161
  pruneEntities,
136
162
  pruneCheckpoints,
137
163
  runConcurrent,