envio 3.5.0-alpha.0 → 3.5.0-alpha.2

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 (92) hide show
  1. package/index.d.ts +10 -6
  2. package/package.json +6 -6
  3. package/src/BatchProcessing.res +13 -0
  4. package/src/BatchProcessing.res.mjs +4 -0
  5. package/src/Bin.res +2 -2
  6. package/src/ChainFetching.res +5 -5
  7. package/src/ChainFetching.res.mjs +3 -4
  8. package/src/ChainState.res +44 -36
  9. package/src/ChainState.res.mjs +36 -31
  10. package/src/ChainState.resi +2 -12
  11. package/src/Change.res +3 -3
  12. package/src/Config.res +4 -4
  13. package/src/Config.res.mjs +1 -10
  14. package/src/Core.res +4 -35
  15. package/src/Core.res.mjs +1 -31
  16. package/src/CrossChainState.res +2 -20
  17. package/src/CrossChainState.res.mjs +2 -6
  18. package/src/EntityId.res +15 -0
  19. package/src/EntityId.res.mjs +9 -0
  20. package/src/Env.res +16 -0
  21. package/src/Env.res.mjs +6 -0
  22. package/src/EventConfigBuilder.res +1 -58
  23. package/src/EventConfigBuilder.res.mjs +1 -33
  24. package/src/FetchState.res +663 -484
  25. package/src/FetchState.res.mjs +491 -440
  26. package/src/HandlerRegister.res +45 -0
  27. package/src/HandlerRegister.res.mjs +43 -0
  28. package/src/InMemoryStore.res +11 -4
  29. package/src/InMemoryTable.res +12 -10
  30. package/src/InMemoryTable.res.mjs +6 -5
  31. package/src/IndexerState.res +59 -2
  32. package/src/IndexerState.res.mjs +48 -3
  33. package/src/IndexerState.resi +2 -0
  34. package/src/Internal.res +17 -8
  35. package/src/LogSelection.res +2 -1
  36. package/src/Metrics.res +37 -6
  37. package/src/Metrics.res.mjs +5 -1
  38. package/src/Persistence.res +1 -1
  39. package/src/PgStorage.res +55 -22
  40. package/src/PgStorage.res.mjs +25 -17
  41. package/src/SimulateItems.res +2 -2
  42. package/src/TestIndexer.res +6 -5
  43. package/src/TestIndexer.res.mjs +3 -2
  44. package/src/UserContext.res +3 -3
  45. package/src/Writing.res +12 -2
  46. package/src/Writing.res.mjs +13 -2
  47. package/src/bindings/ClickHouse.res +5 -3
  48. package/src/bindings/ClickHouse.res.mjs +2 -5
  49. package/src/bindings/Vitest.res +22 -1
  50. package/src/bindings/Vitest.res.mjs +15 -0
  51. package/src/db/EntityHistory.res +16 -7
  52. package/src/db/EntityHistory.res.mjs +7 -6
  53. package/src/db/Table.res +40 -9
  54. package/src/db/Table.res.mjs +44 -6
  55. package/src/sources/AddressSet.res +48 -0
  56. package/src/sources/AddressSet.res.mjs +11 -0
  57. package/src/sources/AddressStore.res +152 -0
  58. package/src/sources/AddressStore.res.mjs +97 -0
  59. package/src/sources/EvmChain.res +3 -0
  60. package/src/sources/EvmChain.res.mjs +4 -2
  61. package/src/sources/EvmHyperSyncSource.res +9 -9
  62. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  63. package/src/sources/EvmRpcClient.res +10 -3
  64. package/src/sources/EvmRpcClient.res.mjs +3 -3
  65. package/src/sources/FuelHyperSync.res +4 -3
  66. package/src/sources/FuelHyperSync.res.mjs +3 -3
  67. package/src/sources/FuelHyperSync.resi +2 -1
  68. package/src/sources/FuelHyperSyncClient.res +12 -6
  69. package/src/sources/FuelHyperSyncClient.res.mjs +2 -2
  70. package/src/sources/FuelHyperSyncSource.res +7 -4
  71. package/src/sources/FuelHyperSyncSource.res.mjs +3 -3
  72. package/src/sources/HyperSync.res +9 -5
  73. package/src/sources/HyperSync.res.mjs +3 -3
  74. package/src/sources/HyperSync.resi +3 -2
  75. package/src/sources/HyperSyncClient.res +24 -10
  76. package/src/sources/HyperSyncClient.res.mjs +4 -4
  77. package/src/sources/RpcSource.res +71 -67
  78. package/src/sources/RpcSource.res.mjs +4 -4
  79. package/src/sources/SimulateSource.res +26 -11
  80. package/src/sources/SimulateSource.res.mjs +13 -5
  81. package/src/sources/Source.res +8 -4
  82. package/src/sources/SourceManager.res +2 -3
  83. package/src/sources/SourceManager.res.mjs +2 -3
  84. package/src/sources/Svm.res +1 -2
  85. package/src/sources/Svm.res.mjs +1 -1
  86. package/src/sources/SvmHyperSyncClient.res +14 -4
  87. package/src/sources/SvmHyperSyncClient.res.mjs +2 -2
  88. package/src/sources/SvmHyperSyncSource.res +11 -6
  89. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  90. package/src/IndexingAddresses.res +0 -108
  91. package/src/IndexingAddresses.res.mjs +0 -101
  92. package/src/IndexingAddresses.resi +0 -34
package/src/Metrics.res CHANGED
@@ -98,12 +98,19 @@ type sourceHeightMetrics = {
98
98
 
99
99
  type t = {
100
100
  startTime: Date.t,
101
+ // Wall clock when this snapshot was built, so a scrape can be dated.
102
+ metricTime: Date.t,
103
+ // Measured monotonically, not as metricTime - startTime: a wall-clock
104
+ // correction would otherwise skew the counter shares derived from it.
105
+ elapsedSeconds: float,
101
106
  targetBufferSize: int,
102
107
  isInReorgThreshold: bool,
103
108
  rollbackEnabled: bool,
104
109
  maxBatchSize: int,
105
110
  preloadSeconds: float,
106
111
  processingSeconds: float,
112
+ processingStalledOnFetchSeconds: float,
113
+ processingStalledOnStorageWriteSeconds: float,
107
114
  rollbackSeconds: float,
108
115
  rollbackCount: int,
109
116
  rollbackEventsCount: float,
@@ -241,6 +248,24 @@ let renderMetrics = (b: builder, metrics: t) => {
241
248
  byLabels->Dict.toArray
242
249
  }
243
250
 
251
+ b->single(
252
+ ~name="envio_process_start_time_seconds",
253
+ ~help="Start time of the process since unix epoch in seconds.",
254
+ ~kind="gauge",
255
+ ~value=metrics.startTime->Date.getTime /. 1000.,
256
+ )
257
+ b->single(
258
+ ~name="envio_process_metric_time_seconds",
259
+ ~help="The time these metrics were collected. Use it to tell how fresh a snapshot is, or to measure rates between two snapshots.",
260
+ ~kind="gauge",
261
+ ~value=metrics.metricTime->Date.getTime /. 1000.,
262
+ )
263
+ b->single(
264
+ ~name="envio_process_elapsed_seconds",
265
+ ~help="How long the indexer has been running. Divide a cumulative seconds metric by this to get the share of the run it took, eg envio_processing_seconds for time spent in event handlers.",
266
+ ~kind="gauge",
267
+ ~value=metrics.elapsedSeconds,
268
+ )
244
269
  b->single(
245
270
  ~name="envio_preload_seconds",
246
271
  ~help="Cumulative time spent on preloading entities during batch processing.",
@@ -253,6 +278,18 @@ let renderMetrics = (b: builder, metrics: t) => {
253
278
  ~kind="counter",
254
279
  ~value=metrics.processingSeconds,
255
280
  )
281
+ b->single(
282
+ ~name="envio_processing_stalled_on_fetch_seconds",
283
+ ~help="Time the indexer had nothing to process while waiting for events to be fetched. A high rate means fetching is the bottleneck: check the data-source latency and whether it can be queried with more concurrency. Waiting at the chain head for new blocks is not counted.",
284
+ ~kind="counter",
285
+ ~value=metrics.processingStalledOnFetchSeconds,
286
+ )
287
+ b->single(
288
+ ~name="envio_processing_stalled_on_storage_write_seconds",
289
+ ~help="Time the indexer paused processing because too many changes were still waiting to be written. A high rate means storage writes are the bottleneck: check envio_storage_write_seconds and the database performance.",
290
+ ~kind="counter",
291
+ ~value=metrics.processingStalledOnStorageWriteSeconds,
292
+ )
256
293
  b->series(
257
294
  ~name="envio_progress_ready",
258
295
  ~help="Whether the chain is fully synced to the head.",
@@ -346,12 +383,6 @@ let renderMetrics = (b: builder, metrics: t) => {
346
383
  ~entries=chains,
347
384
  ~value=m => m.sourceBlockNumber->Int.toFloat,
348
385
  )
349
- b->single(
350
- ~name="envio_process_start_time_seconds",
351
- ~help="Start time of the process since unix epoch in seconds.",
352
- ~kind="gauge",
353
- ~value=metrics.startTime->Date.getTime /. 1000.,
354
- )
355
386
  b->series(
356
387
  ~name="envio_indexing_concurrency",
357
388
  ~help="The number of executing concurrent queries to the chain data-source.",
@@ -108,8 +108,13 @@ function renderMetrics(b, metrics) {
108
108
  }
109
109
  });
110
110
  let sources = Object.entries(byLabels$1);
111
+ single(b, "envio_process_start_time_seconds", "Start time of the process since unix epoch in seconds.", "gauge", metrics.startTime.getTime() / 1000);
112
+ single(b, "envio_process_metric_time_seconds", "The time these metrics were collected. Use it to tell how fresh a snapshot is, or to measure rates between two snapshots.", "gauge", metrics.metricTime.getTime() / 1000);
113
+ single(b, "envio_process_elapsed_seconds", "How long the indexer has been running. Divide a cumulative seconds metric by this to get the share of the run it took, eg envio_processing_seconds for time spent in event handlers.", "gauge", metrics.elapsedSeconds);
111
114
  single(b, "envio_preload_seconds", "Cumulative time spent on preloading entities during batch processing.", "counter", metrics.preloadSeconds);
112
115
  single(b, "envio_processing_seconds", "Cumulative time spent executing event handlers during batch processing.", "counter", metrics.processingSeconds);
116
+ single(b, "envio_processing_stalled_on_fetch_seconds", "Time the indexer had nothing to process while waiting for events to be fetched. A high rate means fetching is the bottleneck: check the data-source latency and whether it can be queried with more concurrency. Waiting at the chain head for new blocks is not counted.", "counter", metrics.processingStalledOnFetchSeconds);
117
+ single(b, "envio_processing_stalled_on_storage_write_seconds", "Time the indexer paused processing because too many changes were still waiting to be written. A high rate means storage writes are the bottleneck: check envio_storage_write_seconds and the database performance.", "counter", metrics.processingStalledOnStorageWriteSeconds);
113
118
  series(b, "envio_progress_ready", "Whether the chain is fully synced to the head.", "gauge", chains, m => {
114
119
  if (m.isReady) {
115
120
  return 1;
@@ -129,7 +134,6 @@ function renderMetrics(b, metrics) {
129
134
  series(b, "envio_fetching_block_range_events_total", "Cumulative number of events fetched across all block range operations.", "counter", chains, m => m.blockRangeFetchedEvents);
130
135
  series(b, "envio_fetching_block_range_size", "Cumulative number of blocks covered across all block range fetch operations.", "counter", chains, m => m.blockRangeFetchedBlocks);
131
136
  series(b, "envio_indexing_known_height", "The latest known block number reported by the active indexing source. This value may lag behind the actual chain height, as it is updated only when needed.", "gauge", chains, m => m.sourceBlockNumber);
132
- single(b, "envio_process_start_time_seconds", "Start time of the process since unix epoch in seconds.", "gauge", metrics.startTime.getTime() / 1000);
133
137
  series(b, "envio_indexing_concurrency", "The number of executing concurrent queries to the chain data-source.", "gauge", chains, m => m.concurrency);
134
138
  series(b, "envio_indexing_partitions", "The number of partitions used to split fetching logic by addresses and block ranges.", "gauge", chains, m => m.partitionsCount);
135
139
  series(b, "envio_indexing_idle_seconds", "The time the indexer source syncing has been idle. A high value may indicate the source sync is a bottleneck.", "counter", chains, m => m.idleSeconds);
@@ -121,7 +121,7 @@ type storage = {
121
121
  getRollbackData: (
122
122
  ~entityConfig: Internal.entityConfig,
123
123
  ~rollbackTargetCheckpointId: Internal.checkpointId,
124
- ) => promise<(array<string>, array<unknown>)>,
124
+ ) => promise<(array<EntityId.t>, array<unknown>)>,
125
125
  // Write batch to storage
126
126
  writeBatch: (
127
127
  ~batch: Batch.t,
package/src/PgStorage.res CHANGED
@@ -158,7 +158,10 @@ let getEntityHistory = (~entityConfig: Internal.entityConfig): EntityHistory.pgE
158
158
  ~fields=dataFields->Array.concat([checkpointIdField, actionField]),
159
159
  )
160
160
 
161
- let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema)
161
+ let setChangeSchema = EntityHistory.makeSetUpdateSchema(
162
+ ~idSchema=entityConfig.table->Table.getIdSchema,
163
+ entityConfig.schema,
164
+ )
162
165
 
163
166
  {
164
167
  EntityHistory.table,
@@ -355,8 +358,8 @@ let makeDeleteByIdQuery = (~pgSchema, ~tableName) => {
355
358
  `DELETE FROM "${pgSchema}"."${tableName}" WHERE id = $1;`
356
359
  }
357
360
 
358
- let makeDeleteByIdsQuery = (~pgSchema, ~tableName) => {
359
- `DELETE FROM "${pgSchema}"."${tableName}" WHERE id = ANY($1::text[]);`
361
+ let makeDeleteByIdsQuery = (~pgSchema, ~tableName, ~idPgType) => {
362
+ `DELETE FROM "${pgSchema}"."${tableName}" WHERE id = ANY($1::${idPgType}[]);`
360
363
  }
361
364
 
362
365
  let makeLoadAllQuery = (~pgSchema, ~tableName) => {
@@ -744,18 +747,26 @@ let getConnectedPsqlExec = {
744
747
  }
745
748
  }
746
749
 
747
- let deleteByIdsOrThrow = async (sql, ~pgSchema, ~ids, ~table: Table.table) => {
750
+ let deleteByIdsOrThrow = async (sql, ~pgSchema, ~ids: array<EntityId.t>, ~table: Table.table) => {
751
+ // A JSON array of the serialized ids. For a single id the query binds it as
752
+ // `$1` directly (the array is the positional-params array); for many it binds
753
+ // the whole array to `$1` behind an `ANY(...)`.
754
+ let idsJson = table->Table.encodeIdsToJson(ids)
748
755
  switch await (
749
756
  switch ids {
750
757
  | [_] =>
751
758
  sql->Postgres.preparedUnsafe(
752
759
  makeDeleteByIdQuery(~pgSchema, ~tableName=table.tableName),
753
- ids->Obj.magic,
760
+ idsJson->Obj.magic,
754
761
  )
755
762
  | _ =>
756
763
  sql->Postgres.preparedUnsafe(
757
- makeDeleteByIdsQuery(~pgSchema, ~tableName=table.tableName),
758
- [ids]->Obj.magic,
764
+ makeDeleteByIdsQuery(
765
+ ~pgSchema,
766
+ ~tableName=table.tableName,
767
+ ~idPgType=table->Table.getIdPgFieldType(~pgSchema),
768
+ ),
769
+ [idsJson]->Obj.magic,
759
770
  )
760
771
  }
761
772
  ) {
@@ -811,9 +822,11 @@ let makeInsertDeleteUpdatesQuery = (~entityConfig: Internal.entityConfig, ~pgSch
811
822
  ~isNullable=false,
812
823
  )
813
824
 
825
+ let idPgType = entityConfig.table->Table.getIdPgFieldType(~pgSchema)
826
+
814
827
  `INSERT INTO "${pgSchema}"."${historyTableName}" (${allHistoryFieldNamesStr})
815
828
  SELECT ${selectPartsStr}
816
- FROM UNNEST($1::text[], $2::${checkpointIdPgType}[]) AS u(${Table.idFieldName}, ${EntityHistory.checkpointIdFieldName})`
829
+ FROM UNNEST($1::${idPgType}[], $2::${checkpointIdPgType}[]) AS u(${Table.idFieldName}, ${EntityHistory.checkpointIdFieldName})`
817
830
  }
818
831
 
819
832
  let executeSet = (
@@ -912,17 +925,21 @@ let rec writeBatch = async (
912
925
  // Single pass over the change log: track each id's latest change (the last
913
926
  // one seen) and, when saving history, fan every non-diff change out to the
914
927
  // history-table batches.
928
+ // Keyed/deduped in memory by the id's string key (toKey), while the
929
+ // batches sent to SQL keep the real id values so they serialize with the
930
+ // id column's type.
915
931
  let latestChangeById = Dict.make()
916
932
  let orderedIds = []
917
933
  changes->Array.forEach(change => {
918
934
  let entityId = change->Change.getEntityId
919
- if latestChangeById->Utils.Dict.dangerouslyGetNonOption(entityId)->Option.isNone {
935
+ let entityKey = entityId->EntityId.toKey
936
+ if latestChangeById->Utils.Dict.dangerouslyGetNonOption(entityKey)->Option.isNone {
920
937
  orderedIds->Array.push(entityId)
921
938
  }
922
- latestChangeById->Dict.set(entityId, change)
939
+ latestChangeById->Dict.set(entityKey, change)
923
940
  if shouldSaveHistory {
924
941
  if Some(change->Change.getCheckpointId) === diffCheckpointId {
925
- idsWithDiff->Utils.Set.add(entityId)->ignore
942
+ idsWithDiff->Utils.Set.add(entityKey)->ignore
926
943
  } else {
927
944
  switch change {
928
945
  | Delete({entityId, checkpointId}) =>
@@ -936,13 +953,14 @@ let rec writeBatch = async (
936
953
 
937
954
  let backfillHistoryIds = Utils.Set.make()
938
955
  orderedIds->Array.forEach(entityId => {
939
- switch latestChangeById->Dict.getUnsafe(entityId) {
956
+ let entityKey = entityId->EntityId.toKey
957
+ switch latestChangeById->Dict.getUnsafe(entityKey) {
940
958
  | Set({entity}) => entitiesToSet->Array.push(entity)
941
959
  | Delete({entityId}) => idsToDelete->Array.push(entityId)
942
960
  }
943
961
 
944
962
  // An id needs a history backfill iff none of its changes is the diff.
945
- if shouldSaveHistory && !(idsWithDiff->Utils.Set.has(entityId)) {
963
+ if shouldSaveHistory && !(idsWithDiff->Utils.Set.has(entityKey)) {
946
964
  backfillHistoryIds->Utils.Set.add(entityId)->ignore
947
965
  }
948
966
  })
@@ -962,7 +980,7 @@ let rec writeBatch = async (
962
980
  await EntityHistory.backfillHistory(
963
981
  sql,
964
982
  ~pgSchema,
965
- ~entityName=entityConfig.name,
983
+ ~table=entityConfig.table,
966
984
  ~entityIndex=entityConfig.index,
967
985
  ~ids=backfillHistoryIds->Utils.Set.toArray,
968
986
  )
@@ -974,7 +992,7 @@ let rec writeBatch = async (
974
992
  ->Postgres.preparedUnsafe(
975
993
  makeInsertDeleteUpdatesQuery(~entityConfig, ~pgSchema),
976
994
  (
977
- batchDeleteEntityIds,
995
+ entityConfig.table->Table.encodeIdsToJson(batchDeleteEntityIds),
978
996
  batchDeleteCheckpointIds->Utils.BigInt.arrayToStringArray,
979
997
  )->Obj.magic,
980
998
  )
@@ -1230,10 +1248,25 @@ let makeGetRollbackRemovedIdsQuery = (~entityConfig: Internal.entityConfig, ~pgS
1230
1248
  )`
1231
1249
  }
1232
1250
 
1233
- let rollbackRowStateSchema = S.object(s => (
1234
- s.field(Table.idFieldName, S.string),
1235
- s.field(EntityHistory.changeFieldName, EntityHistory.RowAction.schema),
1236
- ))
1251
+ // Memoized per table so the id is parsed with that entity's id schema (a
1252
+ // numeric id comes back from Postgres as a number, not a string) and the
1253
+ // schema's operations compile once rather than per rollback row.
1254
+ let rollbackRowStateSchema: Table.table => S.t<(
1255
+ EntityId.t,
1256
+ EntityHistory.RowAction.t,
1257
+ )> = Utils.WeakMap.memoize(table =>
1258
+ S.object(s => (
1259
+ s.field(Table.idFieldName, table->Table.getIdSchema),
1260
+ s.field(EntityHistory.changeFieldName, EntityHistory.RowAction.schema),
1261
+ ))
1262
+ )
1263
+
1264
+ // Same reason as above for the id-only rows: both rollback queries must yield
1265
+ // ids in the entity's own representation, or the two halves of the diff would
1266
+ // disagree (Postgres hands back a NUMERIC id as a string, not a bigint).
1267
+ let rollbackRemovedIdsSchema: Table.table => S.t<array<EntityId.t>> = Utils.WeakMap.memoize(table =>
1268
+ S.array(S.object(s => s.field(Table.idFieldName, table->Table.getIdSchema)))
1269
+ )
1237
1270
 
1238
1271
  let make = (
1239
1272
  ~sql: Postgres.sql,
@@ -1757,7 +1790,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1757
1790
  makeGetRollbackRemovedIdsQuery(~entityConfig, ~pgSchema),
1758
1791
  [rollbackTargetCheckpointId->BigInt.toString]->(Utils.magic: array<string> => unknown),
1759
1792
  )
1760
- ->(Utils.magic: promise<unknown> => promise<array<{"id": string}>>),
1793
+ ->(Utils.magic: promise<unknown> => promise<array<unknown>>),
1761
1794
  // Get the latest pre-target row, including its SET or DELETE action.
1762
1795
  sql
1763
1796
  ->Postgres.preparedUnsafe(
@@ -1767,10 +1800,10 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1767
1800
  ->(Utils.magic: promise<unknown> => promise<array<unknown>>),
1768
1801
  ))
1769
1802
 
1770
- let removedIds = removedIdRows->Array.map(row => row["id"])
1803
+ let removedIds = removedIdRows->S.parseOrThrow(rollbackRemovedIdsSchema(entityConfig.table))
1771
1804
  let restoredEntitiesResult = []
1772
1805
  rollbackRows->Array.forEach(row => {
1773
- let (entityId, action) = row->S.parseOrThrow(rollbackRowStateSchema)
1806
+ let (entityId, action) = row->S.parseOrThrow(rollbackRowStateSchema(entityConfig.table))
1774
1807
  switch action {
1775
1808
  | SET => restoredEntitiesResult->Array.push(row)->ignore
1776
1809
  | DELETE => removedIds->Array.push(entityId)->ignore
@@ -10,6 +10,7 @@ import * as Config from "./Config.res.mjs";
10
10
  import * as Hasura from "./Hasura.res.mjs";
11
11
  import * as Schema from "./db/Schema.res.mjs";
12
12
  import * as Logging from "./Logging.res.mjs";
13
+ import * as EntityId from "./EntityId.res.mjs";
13
14
  import * as Internal from "./Internal.res.mjs";
14
15
  import Postgres from "postgres";
15
16
  import * as ChainState from "./ChainState.res.mjs";
@@ -157,7 +158,7 @@ function getEntityHistory(entityConfig) {
157
158
  checkpointIdField,
158
159
  actionField
159
160
  ]), undefined);
160
- let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema);
161
+ let setChangeSchema = EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), entityConfig.schema);
161
162
  let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema);
162
163
  let cache$1 = {
163
164
  table: table,
@@ -292,8 +293,8 @@ function makeDeleteByIdQuery(pgSchema, tableName) {
292
293
  return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = $1;`;
293
294
  }
294
295
 
295
- function makeDeleteByIdsQuery(pgSchema, tableName) {
296
- return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1::text[]);`;
296
+ function makeDeleteByIdsQuery(pgSchema, tableName, idPgType) {
297
+ return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1::` + idPgType + `[]);`;
297
298
  }
298
299
 
299
300
  function makeLoadAllQuery(pgSchema, tableName) {
@@ -547,9 +548,10 @@ async function getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containe
547
548
  }
548
549
 
549
550
  async function deleteByIdsOrThrow(sql, pgSchema, ids, table) {
551
+ let idsJson = Table.encodeIdsToJson(table, ids);
550
552
  try {
551
553
  await (
552
- ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName), [ids], {prepare: true}) : sql.unsafe(makeDeleteByIdQuery(pgSchema, table.tableName), ids, {prepare: true})
554
+ ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName, Table.getIdPgFieldType(table, pgSchema)), [idsJson], {prepare: true}) : sql.unsafe(makeDeleteByIdQuery(pgSchema, table.tableName), idsJson, {prepare: true})
553
555
  );
554
556
  return;
555
557
  } catch (raw_exn) {
@@ -586,9 +588,10 @@ function makeInsertDeleteUpdatesQuery(entityConfig, pgSchema) {
586
588
  });
587
589
  let selectPartsStr = selectParts.join(", ");
588
590
  let checkpointIdPgType = Table.getPgFieldType(EntityHistory.checkpointIdFieldType, pgSchema, false, false, false);
591
+ let idPgType = Table.getIdPgFieldType(entityConfig.table, pgSchema);
589
592
  return `INSERT INTO "` + pgSchema + `"."` + historyTableName + `" (` + allHistoryFieldNamesStr + `)
590
593
  SELECT ` + selectPartsStr + `
591
- FROM UNNEST($1::text[], $2::` + checkpointIdPgType + `[]) AS u(` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`;
594
+ FROM UNNEST($1::` + idPgType + `[], $2::` + checkpointIdPgType + `[]) AS u(` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`;
592
595
  }
593
596
 
594
597
  function executeSet(sql, items, dbFunction) {
@@ -648,15 +651,16 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
648
651
  let orderedIds = [];
649
652
  param.changes.forEach(change => {
650
653
  let entityId = change.entityId;
651
- if (Stdlib_Option.isNone(latestChangeById[entityId])) {
654
+ let entityKey = EntityId.toKey(entityId);
655
+ if (Stdlib_Option.isNone(latestChangeById[entityKey])) {
652
656
  orderedIds.push(entityId);
653
657
  }
654
- latestChangeById[entityId] = change;
658
+ latestChangeById[entityKey] = change;
655
659
  if (!shouldSaveHistory) {
656
660
  return;
657
661
  }
658
662
  if (change.checkpointId === diffCheckpointId) {
659
- idsWithDiff.add(entityId);
663
+ idsWithDiff.add(entityKey);
660
664
  return;
661
665
  }
662
666
  if (change.type === "SET") {
@@ -668,13 +672,14 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
668
672
  });
669
673
  let backfillHistoryIds = new Set();
670
674
  orderedIds.forEach(entityId => {
671
- let match = latestChangeById[entityId];
675
+ let entityKey = EntityId.toKey(entityId);
676
+ let match = latestChangeById[entityKey];
672
677
  if (match.type === "SET") {
673
678
  entitiesToSet.push(match.entity);
674
679
  } else {
675
680
  idsToDelete.push(match.entityId);
676
681
  }
677
- if (shouldSaveHistory && !idsWithDiff.has(entityId)) {
682
+ if (shouldSaveHistory && !idsWithDiff.has(entityKey)) {
678
683
  backfillHistoryIds.add(entityId);
679
684
  return;
680
685
  }
@@ -685,11 +690,11 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
685
690
  let promises = [];
686
691
  if (shouldSaveHistory) {
687
692
  if (backfillHistoryIds.size !== 0) {
688
- await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.name, entityConfig.index, Array.from(backfillHistoryIds));
693
+ await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.table, entityConfig.index, Array.from(backfillHistoryIds));
689
694
  }
690
695
  if (Utils.$$Array.notEmpty(batchDeleteCheckpointIds)) {
691
696
  promises.push(sql.unsafe(makeInsertDeleteUpdatesQuery(entityConfig, pgSchema), [
692
- batchDeleteEntityIds,
697
+ Table.encodeIdsToJson(entityConfig.table, batchDeleteEntityIds),
693
698
  Utils.$$BigInt.arrayToStringArray(batchDeleteCheckpointIds)
694
699
  ], {prepare: true}));
695
700
  }
@@ -822,10 +827,12 @@ function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) {
822
827
  )`;
823
828
  }
824
829
 
825
- let rollbackRowStateSchema = S$RescriptSchema.object(s => [
826
- s.f(Table.idFieldName, S$RescriptSchema.string),
830
+ let rollbackRowStateSchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.object(s => [
831
+ s.f(Table.idFieldName, Table.getIdSchema(table)),
827
832
  s.f(EntityHistory.changeFieldName, EntityHistory.RowAction.schema)
828
- ]);
833
+ ]));
834
+
835
+ let rollbackRemovedIdsSchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.array(S$RescriptSchema.object(s => s.f(Table.idFieldName, Table.getIdSchema(table)))));
829
836
 
830
837
  function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isHasuraEnabled, sink, onInitialize, onNewTables) {
831
838
  let containerName = "envio-postgres";
@@ -1147,10 +1154,10 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1147
1154
  sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1148
1155
  sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1149
1156
  ]);
1150
- let removedIds = match[0].map(row => row.id);
1157
+ let removedIds = S$RescriptSchema.parseOrThrow(match[0], rollbackRemovedIdsSchema(entityConfig.table));
1151
1158
  let restoredEntitiesResult = [];
1152
1159
  match[1].forEach(row => {
1153
- let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema);
1160
+ let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema(entityConfig.table));
1154
1161
  if (match[1] === "SET") {
1155
1162
  restoredEntitiesResult.push(row);
1156
1163
  return;
@@ -1297,6 +1304,7 @@ export {
1297
1304
  makeGetRollbackPreTargetRowsQuery,
1298
1305
  makeGetRollbackRemovedIdsQuery,
1299
1306
  rollbackRowStateSchema,
1307
+ rollbackRemovedIdsSchema,
1300
1308
  make,
1301
1309
  makeStorageFromEnv,
1302
1310
  makePersistenceFromConfig,
@@ -343,8 +343,8 @@ let parse = (
343
343
  // run here the way real routing does (one item per registration).
344
344
  // Registrations are built the same way `HandlerRegister.finishRegistration`
345
345
  // does at startup (not stubs), so the address filter and `where` behave
346
- // identically to real indexing — the dead-input tracker relies on
347
- // `clientAddressFilter` actually gating unrouted items. Drop registrations
346
+ // identically to real indexing — the dead-input tracker relies on the
347
+ // source's address gate actually dropping unrouted items. Drop registrations
348
348
  // whose `where` excludes this chain (they wouldn't be fetched live), and
349
349
  // ignore the bare handler-less fallback so a missing handler surfaces below
350
350
  // instead of silently running nothing.
@@ -22,7 +22,7 @@ type t<'processConfig> = {process: 'processConfig => promise<processResult>}
22
22
 
23
23
  type entityChange = {
24
24
  sets: array<unknown>,
25
- deleted: array<string>,
25
+ deleted: array<EntityId.t>,
26
26
  }
27
27
 
28
28
  type testIndexerState = {
@@ -136,11 +136,12 @@ let handleWriteBatch = (
136
136
  switch change {
137
137
  | Set({entityId, entity, checkpointId}) =>
138
138
  // The store keeps decoded entities so load comparisons (bigint /
139
- // BigDecimal) work on real values.
140
- entityDict->Dict.set(entityId, entity)
139
+ // BigDecimal) work on real values. Ids are keyed by their string form
140
+ // since they may be string/int/bigint.
141
+ entityDict->Dict.set(entityId->EntityId.toKey, entity)
141
142
  entityChangeFor(checkpointId).sets->Array.push(entity->Utils.magic)->ignore
142
143
  | Delete({entityId, checkpointId}) =>
143
- Dict.delete(entityDict->Obj.magic, entityId)
144
+ Dict.delete(entityDict->Obj.magic, entityId->EntityId.toKey)
144
145
  entityChangeFor(checkpointId).deleted->Array.push(entityId)->ignore
145
146
  }
146
147
  }
@@ -197,7 +198,7 @@ let handleWriteBatch = (
197
198
  entityObj->Dict.set("sets", sets->(Utils.magic: array<unknown> => unknown))
198
199
  }
199
200
  if deleted->Array.length > 0 {
200
- entityObj->Dict.set("deleted", deleted->(Utils.magic: array<string> => unknown))
201
+ entityObj->Dict.set("deleted", deleted->(Utils.magic: array<EntityId.t> => unknown))
201
202
  }
202
203
  // Match the capitalized entity accessor the generated change types expose.
203
204
  change->Dict.set(
@@ -5,6 +5,7 @@ import * as Utils from "./Utils.res.mjs";
5
5
  import * as Config from "./Config.res.mjs";
6
6
  import * as Logging from "./Logging.res.mjs";
7
7
  import * as ChainMap from "./ChainMap.res.mjs";
8
+ import * as EntityId from "./EntityId.res.mjs";
8
9
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
9
10
  import * as IndexerLoop from "./IndexerLoop.res.mjs";
10
11
  import * as Persistence from "./Persistence.res.mjs";
@@ -102,12 +103,12 @@ function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChain
102
103
  let processChange = change => {
103
104
  if (change.type === "SET") {
104
105
  let entity = change.entity;
105
- entityDict[change.entityId] = entity;
106
+ entityDict[EntityId.toKey(change.entityId)] = entity;
106
107
  entityChangeFor(change.checkpointId).sets.push(entity);
107
108
  return;
108
109
  }
109
110
  let entityId = change.entityId;
110
- Stdlib_Dict.$$delete(entityDict, entityId);
111
+ Stdlib_Dict.$$delete(entityDict, EntityId.toKey(entityId));
111
112
  entityChangeFor(change.checkpointId).deleted.push(entityId);
112
113
  };
113
114
  param.changes.forEach(processChange);
@@ -144,7 +144,7 @@ let getWhereHandler = (params: entityContextParams, filter: dict<dict<unknown>>)
144
144
  }
145
145
 
146
146
  let noopSet = (_entity: Internal.entity) => ()
147
- let noopDeleteUnsafe = (_entityId: string) => ()
147
+ let noopDeleteUnsafe = (_entityId: EntityId.t) => ()
148
148
 
149
149
  // Reads against ClickHouse-only entities have no Postgres table to hit;
150
150
  // surface a friendly error instead of letting the SQL layer fail with
@@ -168,7 +168,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
168
168
  ->InMemoryTable.Entity.set(
169
169
  ~committedCheckpointId=params.indexerState->IndexerState.committedCheckpointId,
170
170
  Set({
171
- entityId: entity.id,
171
+ entityId: entity.id->EntityId.unsafeOfString,
172
172
  checkpointId: params.checkpointId,
173
173
  entity,
174
174
  }),
@@ -282,7 +282,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
282
282
  }),
283
283
  )
284
284
  }
285
- }->(Utils.magic: (string => unit) => unknown)
285
+ }->(Utils.magic: (EntityId.t => unit) => unknown)
286
286
  | _ =>
287
287
  JsError.throwWithMessage(`Invalid context.${params.entityConfig.name}.${prop} operation.`)
288
288
  }
package/src/Writing.res CHANGED
@@ -237,7 +237,7 @@ let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
237
237
 
238
238
  // Blocks until the store holds fewer than keepLatestChangesLimit changes,
239
239
  // freeing committed changes first and awaiting commits as a last resort.
240
- let rec awaitCapacity = async (state: IndexerState.t) => {
240
+ let rec awaitCapacityLoop = async (state: IndexerState.t) => {
241
241
  // After a failed write nothing will free capacity, so bail instead of waiting
242
242
  // on a commit that won't come (the error already went to onError).
243
243
  if !(state->IndexerState.hasFailedWrite) && state->getChangesCount >= keepLatestChangesLimit {
@@ -259,11 +259,21 @@ let rec awaitCapacity = async (state: IndexerState.t) => {
259
259
  ) {
260
260
  state->schedule
261
261
  await state->waitForCommit
262
- await state->awaitCapacity
262
+ await state->awaitCapacityLoop
263
263
  }
264
264
  }
265
265
  }
266
266
 
267
+ // Only the over-limit path is a real stall. Timing every call would charge each
268
+ // batch the microtask hop of awaiting an already-resolved promise, which reads
269
+ // as storage backpressure that isn't there.
270
+ let awaitCapacity = async (state: IndexerState.t) =>
271
+ if state->getChangesCount >= keepLatestChangesLimit {
272
+ let timeRef = Performance.now()
273
+ await state->awaitCapacityLoop
274
+ state->IndexerState.recordStalledOnStorageWrite(~seconds=timeRef->Performance.secondsSince)
275
+ }
276
+
267
277
  // Awaits until everything processed is persisted. On a failed write we stop
268
278
  // draining (onError already surfaced it) rather than throw.
269
279
  let rec flush = async (state: IndexerState.t) => {
@@ -4,6 +4,7 @@ import * as Env from "./Env.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
5
  import * as Throttler from "./Throttler.res.mjs";
6
6
  import * as EffectState from "./EffectState.res.mjs";
7
+ import * as Performance from "./bindings/Performance.res.mjs";
7
8
  import * as IndexerState from "./IndexerState.res.mjs";
8
9
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
10
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
@@ -182,7 +183,7 @@ function dropCommitted(state, keepLoadedFromDb) {
182
183
  EffectState.forEach(IndexerState.effectState(state), inMemTable => InMemoryStore.dropCommittedEffects(inMemTable, committedCheckpointId, keepLoadedFromDb));
183
184
  }
184
185
 
185
- async function awaitCapacity(state) {
186
+ async function awaitCapacityLoop(state) {
186
187
  if (!IndexerState.hasFailedWrite(state) && getChangesCount(state) >= Env.inMemoryObjectsTarget) {
187
188
  dropCommitted(state, true);
188
189
  if (getChangesCount(state) >= Env.inMemoryObjectsTarget) {
@@ -191,13 +192,22 @@ async function awaitCapacity(state) {
191
192
  if (getChangesCount(state) >= Env.inMemoryObjectsTarget && Utils.$$Array.notEmpty(IndexerState.processedBatches(state))) {
192
193
  schedule(state);
193
194
  await new Promise((resolve, param) => IndexerState.addCommitWaiter(state, resolve));
194
- return await awaitCapacity(state);
195
+ return await awaitCapacityLoop(state);
195
196
  } else {
196
197
  return;
197
198
  }
198
199
  }
199
200
  }
200
201
 
202
+ async function awaitCapacity(state) {
203
+ if (getChangesCount(state) < Env.inMemoryObjectsTarget) {
204
+ return;
205
+ }
206
+ let timeRef = Performance.now();
207
+ await awaitCapacityLoop(state);
208
+ return IndexerState.recordStalledOnStorageWrite(state, Performance.secondsSince(timeRef));
209
+ }
210
+
201
211
  async function flush(state) {
202
212
  if (IndexerState.hasFailedWrite(state)) {
203
213
  return;
@@ -224,6 +234,7 @@ export {
224
234
  setChainMeta,
225
235
  commitBatch,
226
236
  dropCommitted,
237
+ awaitCapacityLoop,
227
238
  awaitCapacity,
228
239
  flush,
229
240
  }
@@ -91,7 +91,6 @@ let getClickHouseFieldType = (
91
91
  ->Array.joinUnsafe(", ")
92
92
  `${enumType}(${enumValues})`
93
93
  }
94
- | Entity(_) => "String"
95
94
  }
96
95
 
97
96
  let baseType = if isArray {
@@ -261,11 +260,14 @@ let setUpdatesOrThrow = async (
261
260
  convertOrThrow: S.compile(
262
261
  S.array(
263
262
  S.union([
264
- EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
263
+ EntityHistory.makeSetUpdateSchema(
264
+ ~idSchema=entityConfig.table->Table.getIdSchema,
265
+ makeClickHouseEntitySchema(entityConfig.table),
266
+ ),
265
267
  S.object(s => {
266
268
  s.tag(EntityHistory.changeFieldName, EntityHistory.RowAction.DELETE)
267
269
  Change.Delete({
268
- entityId: s.field(Table.idFieldName, S.string),
270
+ entityId: s.field(Table.idFieldName, entityConfig.table->Table.getIdSchema),
269
271
  checkpointId: s.field(
270
272
  EntityHistory.checkpointIdFieldName,
271
273
  EntityHistory.unsafeCheckpointIdSchema,
@@ -68,9 +68,6 @@ function getClickHouseFieldType(fieldType, isNullable, isArray) {
68
68
  let enumValues = config$1.variants.map(variant => `'` + variant + `'`).join(", ");
69
69
  baseType = enumType + `(` + enumValues + `)`;
70
70
  break;
71
- case "Entity" :
72
- baseType = "String";
73
- break;
74
71
  }
75
72
  }
76
73
  let baseType$1 = isArray ? `Array(` + baseType + `)` : baseType;
@@ -203,12 +200,12 @@ async function setUpdatesOrThrow(client, cache, changes, entityConfig, database)
203
200
  } else {
204
201
  let cached_tableName = database + `.\`` + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + `\``;
205
202
  let cached_convertOrThrow = S$RescriptSchema.compile(S$RescriptSchema.array(S$RescriptSchema.union([
206
- EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
203
+ EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), makeClickHouseEntitySchema(entityConfig.table)),
207
204
  S$RescriptSchema.object(s => {
208
205
  s.tag(EntityHistory.changeFieldName, "DELETE");
209
206
  return {
210
207
  type: "DELETE",
211
- entityId: s.f(Table.idFieldName, S$RescriptSchema.string),
208
+ entityId: s.f(Table.idFieldName, Table.getIdSchema(entityConfig.table)),
212
209
  checkpointId: s.f(EntityHistory.checkpointIdFieldName, EntityHistory.unsafeCheckpointIdSchema)
213
210
  };
214
211
  })