envio 3.3.0-alpha.9 → 3.3.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 (94) hide show
  1. package/index.d.ts +19 -0
  2. package/licenses/CLA.md +35 -0
  3. package/licenses/EULA.md +67 -0
  4. package/licenses/LICENSE.md +45 -0
  5. package/licenses/README.md +35 -0
  6. package/package.json +9 -8
  7. package/src/Batch.res.mjs +1 -1
  8. package/src/BatchProcessing.res +0 -7
  9. package/src/BatchProcessing.res.mjs +1 -8
  10. package/src/ChainFetching.res +30 -18
  11. package/src/ChainFetching.res.mjs +23 -13
  12. package/src/ChainState.res +134 -55
  13. package/src/ChainState.res.mjs +78 -36
  14. package/src/ChainState.resi +14 -2
  15. package/src/Config.res +9 -5
  16. package/src/Config.res.mjs +2 -2
  17. package/src/Core.res +20 -0
  18. package/src/Core.res.mjs +12 -0
  19. package/src/CrossChainState.res +115 -34
  20. package/src/CrossChainState.res.mjs +37 -14
  21. package/src/EffectState.res +100 -0
  22. package/src/EffectState.res.mjs +74 -0
  23. package/src/EffectState.resi +32 -0
  24. package/src/Envio.res +25 -7
  25. package/src/Envio.res.mjs +15 -19
  26. package/src/EventConfigBuilder.res +21 -46
  27. package/src/EventConfigBuilder.res.mjs +18 -25
  28. package/src/EventProcessing.res +8 -5
  29. package/src/EventProcessing.res.mjs +2 -1
  30. package/src/FetchState.res +814 -467
  31. package/src/FetchState.res.mjs +541 -369
  32. package/src/HandlerRegister.res +3 -1
  33. package/src/HandlerRegister.res.mjs +3 -2
  34. package/src/InMemoryStore.res +14 -26
  35. package/src/InMemoryStore.res.mjs +6 -19
  36. package/src/IndexerState.res +15 -39
  37. package/src/IndexerState.res.mjs +18 -29
  38. package/src/IndexerState.resi +2 -10
  39. package/src/Internal.res +98 -14
  40. package/src/Internal.res.mjs +96 -2
  41. package/src/LoadLayer.res +44 -24
  42. package/src/LoadLayer.res.mjs +43 -20
  43. package/src/LoadLayer.resi +1 -0
  44. package/src/LogSelection.res +0 -62
  45. package/src/LogSelection.res.mjs +0 -74
  46. package/src/Metrics.res +1 -1
  47. package/src/Metrics.res.mjs +1 -1
  48. package/src/Persistence.res +12 -3
  49. package/src/PgStorage.res +155 -82
  50. package/src/PgStorage.res.mjs +130 -77
  51. package/src/Prometheus.res +20 -10
  52. package/src/Prometheus.res.mjs +22 -10
  53. package/src/PruneStaleHistory.res +146 -35
  54. package/src/PruneStaleHistory.res.mjs +114 -20
  55. package/src/RawEvent.res +2 -2
  56. package/src/Rollback.res +32 -13
  57. package/src/Rollback.res.mjs +24 -11
  58. package/src/SimulateDeadInputTracker.res +1 -1
  59. package/src/SimulateItems.res +38 -6
  60. package/src/SimulateItems.res.mjs +28 -9
  61. package/src/TestIndexer.res +2 -2
  62. package/src/TestIndexer.res.mjs +2 -2
  63. package/src/TopicFilter.res +1 -25
  64. package/src/TopicFilter.res.mjs +0 -74
  65. package/src/UserContext.res +62 -23
  66. package/src/UserContext.res.mjs +26 -6
  67. package/src/Writing.res +60 -21
  68. package/src/Writing.res.mjs +27 -9
  69. package/src/bindings/NodeJs.res +5 -0
  70. package/src/bindings/Viem.res +0 -5
  71. package/src/sources/EventRouter.res +0 -21
  72. package/src/sources/EventRouter.res.mjs +0 -12
  73. package/src/sources/EvmChain.res +2 -27
  74. package/src/sources/EvmChain.res.mjs +2 -23
  75. package/src/sources/EvmRpcClient.res +58 -23
  76. package/src/sources/EvmRpcClient.res.mjs +11 -5
  77. package/src/sources/HyperSync.res +9 -38
  78. package/src/sources/HyperSync.res.mjs +16 -28
  79. package/src/sources/HyperSync.resi +2 -2
  80. package/src/sources/HyperSyncClient.res +88 -11
  81. package/src/sources/HyperSyncClient.res.mjs +39 -6
  82. package/src/sources/HyperSyncSource.res +18 -199
  83. package/src/sources/HyperSyncSource.res.mjs +9 -128
  84. package/src/sources/Rpc.res +0 -32
  85. package/src/sources/Rpc.res.mjs +1 -46
  86. package/src/sources/RpcSource.res +129 -522
  87. package/src/sources/RpcSource.res.mjs +161 -366
  88. package/src/sources/SimulateSource.res +37 -19
  89. package/src/sources/SimulateSource.res.mjs +27 -10
  90. package/src/sources/SourceManager.res +3 -7
  91. package/src/sources/SourceManager.res.mjs +2 -7
  92. package/src/sources/SourceManager.resi +0 -2
  93. package/src/sources/SvmHyperSyncSource.res +13 -3
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
package/src/PgStorage.res CHANGED
@@ -654,8 +654,6 @@ let makeSchemaTableNamesQuery = (~pgSchema) => {
654
654
  `SELECT table_name FROM information_schema.tables WHERE table_schema = '${pgSchema}';`
655
655
  }
656
656
 
657
- let cacheTablePrefixLength = Internal.cacheTablePrefix->String.length
658
-
659
657
  type schemaCacheTableInfo = {
660
658
  @as("table_name")
661
659
  tableName: string,
@@ -663,13 +661,24 @@ type schemaCacheTableInfo = {
663
661
  count: int,
664
662
  }
665
663
 
664
+ // Matches both the cross-chain (`envio_effect_<name>`) and chain-scoped
665
+ // (`envio_<chainId>_effect_<name>`) cache-table formats. Kept in sync with
666
+ // Internal.EffectCache.
666
667
  let makeSchemaCacheTableInfoQuery = (~pgSchema) => {
667
- `SELECT
668
+ // The column guard requires the effect-cache shape (exactly an `id` + `output`
669
+ // pair) so a user entity table that happens to match the name pattern is never
670
+ // mistaken for an effect cache.
671
+ `SELECT
668
672
  t.table_name,
669
673
  ${getCacheRowCountFnName}(t.table_name) as count
670
674
  FROM information_schema.tables t
671
- WHERE t.table_schema = '${pgSchema}'
672
- AND t.table_name LIKE '${Internal.cacheTablePrefix}%';`
675
+ WHERE t.table_schema = '${pgSchema}'
676
+ AND t.table_name ~ '^envio_([0-9]+_)?effect_.+'
677
+ AND (
678
+ SELECT array_agg(c.column_name::text ORDER BY c.column_name::text)
679
+ FROM information_schema.columns c
680
+ WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name
681
+ ) = ARRAY['id', 'output'];`
673
682
  }
674
683
 
675
684
  type psqlExecState =
@@ -1111,8 +1120,10 @@ let rec writeBatch = async (
1111
1120
  // Since effect cache currently doesn't support rollback,
1112
1121
  // we can run it outside of the transaction for simplicity.
1113
1122
  updatedEffectsCache
1114
- ->Array.map(({effect, items, shouldInitialize}: Persistence.updatedEffectCache) => {
1115
- setEffectCacheOrThrow(~effect, ~items, ~initialize=shouldInitialize)
1123
+ ->Array.map((
1124
+ {table, itemSchema, items, shouldInitialize}: Persistence.updatedEffectCache,
1125
+ ) => {
1126
+ setEffectCacheOrThrow(~table, ~itemSchema, ~items, ~initialize=shouldInitialize)
1116
1127
  })
1117
1128
  ->Promise.all,
1118
1129
  ))
@@ -1157,9 +1168,9 @@ let rec writeBatch = async (
1157
1168
  }
1158
1169
  }
1159
1170
 
1160
- // Returns the most recent entity state for IDs that need to be restored during rollback.
1161
- // For each ID modified after the rollback target, retrieves its latest state at or before the target.
1162
- let makeGetRollbackRestoredEntitiesQuery = (~entityConfig: Internal.entityConfig, ~pgSchema) => {
1171
+ // Returns the most recent history row at or before the rollback target for IDs changed after it.
1172
+ // envio_change is included so ReScript can turn SET rows into restores and DELETE rows into removals.
1173
+ let makeGetRollbackPreTargetRowsQuery = (~entityConfig: Internal.entityConfig, ~pgSchema) => {
1163
1174
  let dataFieldNames = entityConfig.table.fields->Array.filterMap(fieldOrDerived =>
1164
1175
  switch fieldOrDerived {
1165
1176
  | Field(field) => field->Table.getPgDbFieldName->Some
@@ -1175,36 +1186,41 @@ let makeGetRollbackRestoredEntitiesQuery = (~entityConfig: Internal.entityConfig
1175
1186
  ~entityIndex=entityConfig.index,
1176
1187
  )
1177
1188
 
1178
- `SELECT DISTINCT ON (${Table.idFieldName}) ${dataFieldsCommaSeparated}
1179
- FROM "${pgSchema}"."${historyTableName}"
1180
- WHERE "${EntityHistory.checkpointIdFieldName}" <= $1
1181
- AND EXISTS (
1182
- SELECT 1
1183
- FROM "${pgSchema}"."${historyTableName}" h
1184
- WHERE h.${Table.idFieldName} = "${historyTableName}".${Table.idFieldName}
1185
- AND h."${EntityHistory.checkpointIdFieldName}" > $1
1186
- )
1187
- ORDER BY ${Table.idFieldName}, "${EntityHistory.checkpointIdFieldName}" DESC`
1189
+ `SELECT DISTINCT ON ("${Table.idFieldName}") ${dataFieldsCommaSeparated}, "${EntityHistory.changeFieldName}"
1190
+ FROM "${pgSchema}"."${historyTableName}"
1191
+ WHERE "${EntityHistory.checkpointIdFieldName}" <= $1
1192
+ AND EXISTS (
1193
+ SELECT 1
1194
+ FROM "${pgSchema}"."${historyTableName}" h
1195
+ WHERE h."${Table.idFieldName}" = "${historyTableName}"."${Table.idFieldName}"
1196
+ AND h."${EntityHistory.checkpointIdFieldName}" > $1
1197
+ )
1198
+ ORDER BY "${Table.idFieldName}", "${EntityHistory.checkpointIdFieldName}" DESC`
1188
1199
  }
1189
1200
 
1190
1201
  // Returns entity IDs that were created after the rollback target and have no history before it.
1191
- // These entities should be deleted during rollback.
1202
+ // DELETE rows at or before the target are returned by the restore query and classified in ReScript.
1192
1203
  let makeGetRollbackRemovedIdsQuery = (~entityConfig: Internal.entityConfig, ~pgSchema) => {
1193
1204
  let historyTableName = EntityHistory.historyTableName(
1194
1205
  ~entityName=entityConfig.name,
1195
1206
  ~entityIndex=entityConfig.index,
1196
1207
  )
1197
- `SELECT DISTINCT ${Table.idFieldName}
1198
- FROM "${pgSchema}"."${historyTableName}"
1199
- WHERE "${EntityHistory.checkpointIdFieldName}" > $1
1200
- AND NOT EXISTS (
1201
- SELECT 1
1202
- FROM "${pgSchema}"."${historyTableName}" h
1203
- WHERE h.${Table.idFieldName} = "${historyTableName}".${Table.idFieldName}
1204
- AND h."${EntityHistory.checkpointIdFieldName}" <= $1
1205
- )`
1208
+ `SELECT DISTINCT "${Table.idFieldName}"
1209
+ FROM "${pgSchema}"."${historyTableName}"
1210
+ WHERE "${EntityHistory.checkpointIdFieldName}" > $1
1211
+ AND NOT EXISTS (
1212
+ SELECT 1
1213
+ FROM "${pgSchema}"."${historyTableName}" h
1214
+ WHERE h."${Table.idFieldName}" = "${historyTableName}"."${Table.idFieldName}"
1215
+ AND h."${EntityHistory.checkpointIdFieldName}" <= $1
1216
+ )`
1206
1217
  }
1207
1218
 
1219
+ let rollbackRowStateSchema = S.object(s => (
1220
+ s.field(Table.idFieldName, S.string),
1221
+ s.field(EntityHistory.changeFieldName, EntityHistory.RowAction.schema),
1222
+ ))
1223
+
1208
1224
  let make = (
1209
1225
  ~sql: Postgres.sql,
1210
1226
  ~pgHost,
@@ -1238,32 +1254,66 @@ let make = (
1238
1254
  envioTables->Utils.Array.notEmpty
1239
1255
  }
1240
1256
 
1241
- let restoreEffectCache = async (~withUpload) => {
1242
- if withUpload {
1243
- // Try to restore cache tables from binary files
1244
- let nothingToUploadErrorMessage = "Nothing to upload."
1245
-
1246
- switch await Promise.all2((
1247
- NodeJs.Fs.Promises.readdir(cacheDirPath)
1248
- ->Promise.thenResolve(e => Ok(e))
1249
- ->Promise.catch(_ => Promise.resolve(Error(nothingToUploadErrorMessage))),
1250
- getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort, ~containerName),
1251
- )) {
1252
- | (Ok(entries), Ok(psqlExec)) => {
1253
- let cacheFiles = entries->Array.filter(entry => {
1254
- entry->String.endsWith(".tsv")
1257
+ // Scans .envio/cache into a list of (cache table, absolute TSV path). Flat
1258
+ // `<name>.tsv` files map to cross-chain caches; a numeric subdirectory
1259
+ // `<chainId>/<name>.tsv` maps to a chain-scoped cache. Exactly one directory
1260
+ // level is supported. A non-numeric directory that contains TSVs is rejected.
1261
+ // Returns [] when .envio/cache doesn't exist.
1262
+ let scanCacheDir = async () => {
1263
+ let topEntries = try {
1264
+ await NodeJs.Fs.Promises.readdir(cacheDirPath)
1265
+ } catch {
1266
+ | _ => []
1267
+ }
1268
+ let result = []
1269
+ let _ = await topEntries
1270
+ ->Array.map(async entry => {
1271
+ let entryPath = NodeJs.Path.join(cacheDirPath, entry)
1272
+ let isDir = (await NodeJs.Fs.Promises.stat(entryPath))->NodeJs.Fs.Promises.statsIsDirectory
1273
+ if isDir {
1274
+ let subEntries = await NodeJs.Fs.Promises.readdir(entryPath)
1275
+ let tsvs = subEntries->Array.filter(sub => sub->String.endsWith(".tsv"))
1276
+ switch Internal.EffectCache.parseChainId(entry) {
1277
+ | Some(chainId) =>
1278
+ tsvs->Array.forEach(sub => {
1279
+ let effectName = sub->String.slice(~start=0, ~end=-4)
1280
+ let table = Internal.makeCacheTable(~effectName, ~scope=Chain(chainId))
1281
+ result
1282
+ ->Array.push((table, NodeJs.Path.join(entryPath, sub)->NodeJs.Path.toString))
1283
+ ->ignore
1255
1284
  })
1285
+ | None =>
1286
+ if tsvs->Utils.Array.notEmpty {
1287
+ JsError.throwWithMessage(
1288
+ `Invalid effect cache directory ".envio/cache/${entry}". Chain cache directories must be named by a numeric chain id (e.g. "1"). Found cache files: ${tsvs->Array.joinUnsafe(
1289
+ ", ",
1290
+ )}.`,
1291
+ )
1292
+ }
1293
+ }
1294
+ } else if entry->String.endsWith(".tsv") {
1295
+ let effectName = entry->String.slice(~start=0, ~end=-4)
1296
+ let table = Internal.makeCacheTable(~effectName, ~scope=CrossChain)
1297
+ result->Array.push((table, entryPath->NodeJs.Path.toString))->ignore
1298
+ }
1299
+ })
1300
+ ->Promise.all
1301
+ result
1302
+ }
1256
1303
 
1257
- let _ = await cacheFiles
1258
- ->Array.map(entry => {
1259
- let effectName = entry->String.slice(~start=0, ~end=-4)
1260
- let table = Internal.makeCacheTable(~effectName)
1261
-
1304
+ let restoreEffectCache = async (~withUpload) => {
1305
+ if withUpload {
1306
+ // Try to restore cache tables from the .envio/cache TSV files
1307
+ switch await scanCacheDir() {
1308
+ | [] => Logging.info("No cache found to upload.")
1309
+ | entries =>
1310
+ switch await getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort, ~containerName) {
1311
+ | Ok(psqlExec) =>
1312
+ let _ = await entries
1313
+ ->Array.map(((table, inputFile)) => {
1262
1314
  sql
1263
1315
  ->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=false))
1264
1316
  ->Promise.then(() => {
1265
- let inputFile = NodeJs.Path.join(cacheDirPath, entry)->NodeJs.Path.toString
1266
-
1267
1317
  let command = `${psqlExec} -c 'COPY "${pgSchema}"."${table.tableName}" FROM STDIN WITH (FORMAT text, HEADER);' < ${inputFile}`
1268
1318
 
1269
1319
  Promise.make(
@@ -1283,14 +1333,8 @@ let make = (
1283
1333
  })
1284
1334
  })
1285
1335
  ->Promise.all
1286
-
1287
1336
  Logging.info("Successfully uploaded cache.")
1288
- }
1289
- | (Error(message), _)
1290
- | (_, Error(message)) =>
1291
- if message === nothingToUploadErrorMessage {
1292
- Logging.info("No cache found to upload.")
1293
- } else {
1337
+ | Error(message) =>
1294
1338
  Logging.error(`Failed to upload cache, continuing without it. ${message}`)
1295
1339
  }
1296
1340
  }
@@ -1315,8 +1359,14 @@ let make = (
1315
1359
 
1316
1360
  let cache = Dict.make()
1317
1361
  cacheTableInfo->Array.forEach(({tableName, count}) => {
1318
- let effectName = tableName->String.slice(~start=cacheTablePrefixLength)
1319
- cache->Dict.set(effectName, ({effectName, count}: Persistence.effectCacheRecord))
1362
+ switch Internal.EffectCache.fromTableName(tableName) {
1363
+ | Some((effectName, scope)) =>
1364
+ cache->Dict.set(
1365
+ tableName,
1366
+ ({effectName, scope, tableName, count}: Persistence.effectCacheRecord),
1367
+ )
1368
+ | None => ()
1369
+ }
1320
1370
  })
1321
1371
  cache
1322
1372
  }
@@ -1480,12 +1530,11 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1480
1530
  }
1481
1531
 
1482
1532
  let setEffectCacheOrThrow = async (
1483
- ~effect: Internal.effect,
1533
+ ~table: Table.table,
1534
+ ~itemSchema,
1484
1535
  ~items: array<Internal.effectCacheItem>,
1485
1536
  ~initialize: bool,
1486
1537
  ) => {
1487
- let {table, itemSchema} = effect.storageMeta
1488
-
1489
1538
  if initialize {
1490
1539
  let _ = await sql->Postgres.unsafe(
1491
1540
  makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=false),
@@ -1531,24 +1580,36 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1531
1580
  )
1532
1581
 
1533
1582
  let promises = cacheTableInfo->Array.map(async ({tableName}) => {
1534
- let cacheName = tableName->String.slice(~start=cacheTablePrefixLength)
1535
- let outputFile =
1536
- NodeJs.Path.join(cacheDirPath, cacheName ++ ".tsv")->NodeJs.Path.toString
1583
+ switch Internal.EffectCache.fromTableName(tableName) {
1584
+ | Some((effectName, scope)) =>
1585
+ // Reverse mapping: chain-scoped caches dump into a per-chain
1586
+ // subdirectory, created here if needed.
1587
+ let outputPath = NodeJs.Path.join(
1588
+ cacheDirPath,
1589
+ Internal.EffectCache.toCachePath(~effectName, ~scope),
1590
+ )
1591
+ let _ = await NodeJs.Fs.Promises.mkdir(
1592
+ ~path=NodeJs.Path.dirname(outputPath->NodeJs.Path.toString),
1593
+ ~options={recursive: true},
1594
+ )
1595
+ let outputFile = outputPath->NodeJs.Path.toString
1537
1596
 
1538
- let command = `${psqlExec} -c 'COPY "${pgSchema}"."${tableName}" TO STDOUT WITH (FORMAT text, HEADER);' > ${outputFile}`
1597
+ let command = `${psqlExec} -c 'COPY "${pgSchema}"."${tableName}" TO STDOUT WITH (FORMAT text, HEADER);' > ${outputFile}`
1539
1598
 
1540
- Promise.make((resolve, reject) => {
1541
- NodeJs.ChildProcess.execWithOptions(
1542
- command,
1543
- psqlExecOptions,
1544
- (~error, ~stdout, ~stderr as _) => {
1545
- switch error {
1546
- | Value(error) => reject(error)
1547
- | Null => resolve(stdout)
1548
- }
1549
- },
1550
- )
1551
- })
1599
+ await Promise.make((resolve, reject) => {
1600
+ NodeJs.ChildProcess.execWithOptions(
1601
+ command,
1602
+ psqlExecOptions,
1603
+ (~error, ~stdout, ~stderr as _) => {
1604
+ switch error {
1605
+ | Value(error) => reject(error)
1606
+ | Null => resolve(stdout)
1607
+ }
1608
+ },
1609
+ )
1610
+ })
1611
+ | None => ""
1612
+ }
1552
1613
  })
1553
1614
 
1554
1615
  let _ = await promises->Promise.all
@@ -1663,7 +1724,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1663
1724
  ~entityConfig: Internal.entityConfig,
1664
1725
  ~rollbackTargetCheckpointId,
1665
1726
  ) => {
1666
- await Promise.all2((
1727
+ let (removedIdRows, rollbackRows) = await Promise.all2((
1667
1728
  // Get IDs of entities that should be deleted (created after rollback target with no prior history)
1668
1729
  sql
1669
1730
  ->Postgres.preparedUnsafe(
@@ -1671,14 +1732,26 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1671
1732
  [rollbackTargetCheckpointId->BigInt.toString]->(Utils.magic: array<string> => unknown),
1672
1733
  )
1673
1734
  ->(Utils.magic: promise<unknown> => promise<array<{"id": string}>>),
1674
- // Get entities that should be restored to their state at or before rollback target
1735
+ // Get the latest pre-target row, including its SET or DELETE action.
1675
1736
  sql
1676
1737
  ->Postgres.preparedUnsafe(
1677
- makeGetRollbackRestoredEntitiesQuery(~entityConfig, ~pgSchema),
1738
+ makeGetRollbackPreTargetRowsQuery(~entityConfig, ~pgSchema),
1678
1739
  [rollbackTargetCheckpointId->BigInt.toString]->(Utils.magic: array<string> => unknown),
1679
1740
  )
1680
1741
  ->(Utils.magic: promise<unknown> => promise<array<unknown>>),
1681
1742
  ))
1743
+
1744
+ let removedIds = removedIdRows->Array.map(row => row["id"])
1745
+ let restoredEntitiesResult = []
1746
+ rollbackRows->Array.forEach(row => {
1747
+ let (entityId, action) = row->S.parseOrThrow(rollbackRowStateSchema)
1748
+ switch action {
1749
+ | SET => restoredEntitiesResult->Array.push(row)->ignore
1750
+ | DELETE => removedIds->Array.push(entityId)->ignore
1751
+ }
1752
+ })
1753
+
1754
+ (removedIds, restoredEntitiesResult)
1682
1755
  }
1683
1756
 
1684
1757
  let writeBatchMethod = async (
@@ -493,15 +493,18 @@ function makeSchemaTableNamesQuery(pgSchema) {
493
493
  return `SELECT table_name FROM information_schema.tables WHERE table_schema = '` + pgSchema + `';`;
494
494
  }
495
495
 
496
- let cacheTablePrefixLength = Internal.cacheTablePrefix.length;
497
-
498
496
  function makeSchemaCacheTableInfoQuery(pgSchema) {
499
- return `SELECT
497
+ return `SELECT
500
498
  t.table_name,
501
499
  ` + getCacheRowCountFnName + `(t.table_name) as count
502
500
  FROM information_schema.tables t
503
- WHERE t.table_schema = '` + pgSchema + `'
504
- AND t.table_name LIKE '` + Internal.cacheTablePrefix + `%';`;
501
+ WHERE t.table_schema = '` + pgSchema + `'
502
+ AND t.table_name ~ '^envio_([0-9]+_)?effect_.+'
503
+ AND (
504
+ SELECT array_agg(c.column_name::text ORDER BY c.column_name::text)
505
+ FROM information_schema.columns c
506
+ WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name
507
+ ) = ARRAY['id', 'output'];`;
505
508
  }
506
509
 
507
510
  let psqlExecState = {
@@ -766,7 +769,7 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
766
769
  }
767
770
  throw exn;
768
771
  }),
769
- Promise.all(updatedEffectsCache.map(param => setEffectCacheOrThrow(param.effect, param.items, param.shouldInitialize)))
772
+ Promise.all(updatedEffectsCache.map(param => setEffectCacheOrThrow(param.table, param.itemSchema, param.items, param.shouldInitialize)))
770
773
  ]);
771
774
  let specificError$1 = specificError.contents;
772
775
  if (specificError$1 === undefined) {
@@ -789,7 +792,7 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
789
792
  }
790
793
  }
791
794
 
792
- function makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema) {
795
+ function makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema) {
793
796
  let dataFieldNames = Stdlib_Array.filterMap(entityConfig.table.fields, fieldOrDerived => {
794
797
  if (fieldOrDerived.TAG === "Field") {
795
798
  return Table.getPgDbFieldName(fieldOrDerived._0);
@@ -797,31 +800,36 @@ function makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema) {
797
800
  });
798
801
  let dataFieldsCommaSeparated = dataFieldNames.map(name => `"` + name + `"`).join(", ");
799
802
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
800
- return `SELECT DISTINCT ON (` + Table.idFieldName + `) ` + dataFieldsCommaSeparated + `
801
- FROM "` + pgSchema + `"."` + historyTableName + `"
802
- WHERE "` + EntityHistory.checkpointIdFieldName + `" <= $1
803
- AND EXISTS (
804
- SELECT 1
805
- FROM "` + pgSchema + `"."` + historyTableName + `" h
806
- WHERE h.` + Table.idFieldName + ` = "` + historyTableName + `".` + Table.idFieldName + `
807
- AND h."` + EntityHistory.checkpointIdFieldName + `" > $1
808
- )
809
- ORDER BY ` + Table.idFieldName + `, "` + EntityHistory.checkpointIdFieldName + `" DESC`;
803
+ return `SELECT DISTINCT ON ("` + Table.idFieldName + `") ` + dataFieldsCommaSeparated + `, "` + EntityHistory.changeFieldName + `"
804
+ FROM "` + pgSchema + `"."` + historyTableName + `"
805
+ WHERE "` + EntityHistory.checkpointIdFieldName + `" <= $1
806
+ AND EXISTS (
807
+ SELECT 1
808
+ FROM "` + pgSchema + `"."` + historyTableName + `" h
809
+ WHERE h."` + Table.idFieldName + `" = "` + historyTableName + `"."` + Table.idFieldName + `"
810
+ AND h."` + EntityHistory.checkpointIdFieldName + `" > $1
811
+ )
812
+ ORDER BY "` + Table.idFieldName + `", "` + EntityHistory.checkpointIdFieldName + `" DESC`;
810
813
  }
811
814
 
812
815
  function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) {
813
816
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
814
- return `SELECT DISTINCT ` + Table.idFieldName + `
815
- FROM "` + pgSchema + `"."` + historyTableName + `"
816
- WHERE "` + EntityHistory.checkpointIdFieldName + `" > $1
817
- AND NOT EXISTS (
818
- SELECT 1
819
- FROM "` + pgSchema + `"."` + historyTableName + `" h
820
- WHERE h.` + Table.idFieldName + ` = "` + historyTableName + `".` + Table.idFieldName + `
821
- AND h."` + EntityHistory.checkpointIdFieldName + `" <= $1
822
- )`;
817
+ return `SELECT DISTINCT "` + Table.idFieldName + `"
818
+ FROM "` + pgSchema + `"."` + historyTableName + `"
819
+ WHERE "` + EntityHistory.checkpointIdFieldName + `" > $1
820
+ AND NOT EXISTS (
821
+ SELECT 1
822
+ FROM "` + pgSchema + `"."` + historyTableName + `" h
823
+ WHERE h."` + Table.idFieldName + `" = "` + historyTableName + `"."` + Table.idFieldName + `"
824
+ AND h."` + EntityHistory.checkpointIdFieldName + `" <= $1
825
+ )`;
823
826
  }
824
827
 
828
+ let rollbackRowStateSchema = S$RescriptSchema.object(s => [
829
+ s.f(Table.idFieldName, S$RescriptSchema.string),
830
+ s.f(EntityHistory.changeFieldName, EntityHistory.RowAction.schema)
831
+ ]);
832
+
825
833
  function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isHasuraEnabled, sink, onInitialize, onNewTables) {
826
834
  let containerName = "envio-postgres";
827
835
  let psqlExecOptions_env = Object.fromEntries([
@@ -839,32 +847,60 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
839
847
  };
840
848
  let cacheDirPath = Path.resolve(".envio", "cache");
841
849
  let isInitialized = async () => Utils.$$Array.notEmpty(await sql.unsafe(`SELECT table_schema FROM information_schema.tables WHERE table_schema = '` + pgSchema + `' AND (table_name = '` + "event_sync_state" + `' OR table_name = '` + InternalTable.Chains.table.tableName + `');`));
850
+ let scanCacheDir = async () => {
851
+ let topEntries;
852
+ try {
853
+ topEntries = await Fs.promises.readdir(cacheDirPath);
854
+ } catch (exn) {
855
+ topEntries = [];
856
+ }
857
+ let result = [];
858
+ await Promise.all(topEntries.map(async entry => {
859
+ let entryPath = Path.join(cacheDirPath, entry);
860
+ let isDir = (await Fs.promises.stat(entryPath)).isDirectory();
861
+ if (isDir) {
862
+ let subEntries = await Fs.promises.readdir(entryPath);
863
+ let tsvs = subEntries.filter(sub => sub.endsWith(".tsv"));
864
+ let chainId = Internal.EffectCache.parseChainId(entry);
865
+ if (chainId !== undefined) {
866
+ tsvs.forEach(sub => {
867
+ let effectName = sub.slice(0, -4);
868
+ let table = Internal.makeCacheTable(effectName, chainId);
869
+ result.push([
870
+ table,
871
+ Path.join(entryPath, sub)
872
+ ]);
873
+ });
874
+ return;
875
+ } else if (Utils.$$Array.notEmpty(tsvs)) {
876
+ return Stdlib_JsError.throwWithMessage(`Invalid effect cache directory ".envio/cache/` + entry + `". Chain cache directories must be named by a numeric chain id (e.g. "1"). Found cache files: ` + tsvs.join(", ") + `.`);
877
+ } else {
878
+ return;
879
+ }
880
+ }
881
+ if (!entry.endsWith(".tsv")) {
882
+ return;
883
+ }
884
+ let effectName = entry.slice(0, -4);
885
+ let table = Internal.makeCacheTable(effectName, "crossChain");
886
+ result.push([
887
+ table,
888
+ entryPath
889
+ ]);
890
+ }));
891
+ return result;
892
+ };
842
893
  let restoreEffectCache = async withUpload => {
843
894
  if (withUpload) {
844
- let nothingToUploadErrorMessage = "Nothing to upload.";
845
- let match = await Promise.all([
846
- Stdlib_Promise.$$catch(Fs.promises.readdir(cacheDirPath).then(e => ({
847
- TAG: "Ok",
848
- _0: e
849
- })), param => Promise.resolve({
850
- TAG: "Error",
851
- _0: nothingToUploadErrorMessage
852
- })),
853
- getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName)
854
- ]);
855
- let exit = 0;
856
- let message;
857
- let entries = match[0];
858
- if (entries.TAG === "Ok") {
859
- let psqlExec = match[1];
895
+ let entries = await scanCacheDir();
896
+ if (entries.length !== 0) {
897
+ let psqlExec = await getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName);
860
898
  if (psqlExec.TAG === "Ok") {
861
899
  let psqlExec$1 = psqlExec._0;
862
- let cacheFiles = entries._0.filter(entry => entry.endsWith(".tsv"));
863
- await Promise.all(cacheFiles.map(entry => {
864
- let effectName = entry.slice(0, -4);
865
- let table = Internal.makeCacheTable(effectName);
900
+ await Promise.all(entries.map(param => {
901
+ let inputFile = param[1];
902
+ let table = param[0];
866
903
  return sql.unsafe(makeCreateTableQuery(table, pgSchema, false)).then(() => {
867
- let inputFile = Path.join(cacheDirPath, entry);
868
904
  let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + table.tableName + `" FROM STDIN WITH (FORMAT text, HEADER);' < ` + inputFile;
869
905
  return new Promise((resolve, reject) => {
870
906
  Child_process.exec(command, psqlExecOptions, (error, stdout, param) => {
@@ -879,19 +915,10 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
879
915
  }));
880
916
  Logging.info("Successfully uploaded cache.");
881
917
  } else {
882
- message = match[1]._0;
883
- exit = 1;
918
+ Logging.error(`Failed to upload cache, continuing without it. ` + psqlExec._0);
884
919
  }
885
920
  } else {
886
- message = entries._0;
887
- exit = 1;
888
- }
889
- if (exit === 1) {
890
- if (message === nothingToUploadErrorMessage) {
891
- Logging.info("No cache found to upload.");
892
- } else {
893
- Logging.error(`Failed to upload cache, continuing without it. ` + message);
894
- }
921
+ Logging.info("No cache found to upload.");
895
922
  }
896
923
  }
897
924
  let cacheTableInfo = await sql.unsafe(makeSchemaCacheTableInfoQuery(pgSchema));
@@ -900,11 +927,17 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
900
927
  }
901
928
  let cache = {};
902
929
  cacheTableInfo.forEach(param => {
903
- let effectName = param.table_name.slice(cacheTablePrefixLength);
904
- cache[effectName] = {
905
- effectName: effectName,
906
- count: param.count
907
- };
930
+ let tableName = param.table_name;
931
+ let match = Internal.EffectCache.fromTableName(tableName);
932
+ if (match !== undefined) {
933
+ cache[tableName] = {
934
+ effectName: match[0],
935
+ scope: match[1],
936
+ tableName: tableName,
937
+ count: param.count
938
+ };
939
+ return;
940
+ }
908
941
  });
909
942
  return cache;
910
943
  };
@@ -998,16 +1031,14 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
998
1031
  }
999
1032
  };
1000
1033
  let setOrThrow$1 = (items, table, itemSchema) => setOrThrow(sql, items, table, itemSchema, pgSchema);
1001
- let setEffectCacheOrThrow = async (effect, items, initialize) => {
1002
- let match = effect.storageMeta;
1003
- let table = match.table;
1034
+ let setEffectCacheOrThrow = async (table, itemSchema, items, initialize) => {
1004
1035
  if (initialize) {
1005
1036
  await sql.unsafe(makeCreateTableQuery(table, pgSchema, false));
1006
1037
  if (onNewTables !== undefined) {
1007
1038
  await onNewTables([table.tableName]);
1008
1039
  }
1009
1040
  }
1010
- return await setOrThrow$1(items, table, match.itemSchema);
1041
+ return await setOrThrow$1(items, table, itemSchema);
1011
1042
  };
1012
1043
  let dumpEffectCache = async () => {
1013
1044
  try {
@@ -1030,10 +1061,16 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1030
1061
  Logging.info(`Dumping cache: ` + cacheTableInfo.map(param => param.table_name + " (" + param.count.toString() + " rows)").join(", "));
1031
1062
  let promises = cacheTableInfo.map(async param => {
1032
1063
  let tableName = param.table_name;
1033
- let cacheName = tableName.slice(cacheTablePrefixLength);
1034
- let outputFile = Path.join(cacheDirPath, cacheName + ".tsv");
1035
- let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + tableName + `" TO STDOUT WITH (FORMAT text, HEADER);' > ` + outputFile;
1036
- return new Promise((resolve, reject) => {
1064
+ let match = Internal.EffectCache.fromTableName(tableName);
1065
+ if (match === undefined) {
1066
+ return "";
1067
+ }
1068
+ let outputPath = Path.join(cacheDirPath, Internal.EffectCache.toCachePath(match[0], match[1]));
1069
+ await Fs.promises.mkdir(Path.dirname(outputPath), {
1070
+ recursive: true
1071
+ });
1072
+ let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + tableName + `" TO STDOUT WITH (FORMAT text, HEADER);' > ` + outputPath;
1073
+ return await new Promise((resolve, reject) => {
1037
1074
  Child_process.exec(command, psqlExecOptions, (error, stdout, param) => {
1038
1075
  if (error === null) {
1039
1076
  return resolve(stdout);
@@ -1097,10 +1134,26 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1097
1134
  let pruneStaleEntityHistory = (entityName, entityIndex, safeCheckpointId) => EntityHistory.pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, safeCheckpointId);
1098
1135
  let getRollbackTargetCheckpoint = (reorgChainId, lastKnownValidBlockNumber) => InternalTable.Checkpoints.getRollbackTargetCheckpoint(sql, pgSchema, reorgChainId, lastKnownValidBlockNumber);
1099
1136
  let getRollbackProgressDiff = rollbackTargetCheckpointId => InternalTable.Checkpoints.getRollbackProgressDiff(sql, pgSchema, rollbackTargetCheckpointId);
1100
- let getRollbackData = async (entityConfig, rollbackTargetCheckpointId) => await Promise.all([
1101
- sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1102
- sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1103
- ]);
1137
+ let getRollbackData = async (entityConfig, rollbackTargetCheckpointId) => {
1138
+ let match = await Promise.all([
1139
+ sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1140
+ sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1141
+ ]);
1142
+ let removedIds = match[0].map(row => row.id);
1143
+ let restoredEntitiesResult = [];
1144
+ match[1].forEach(row => {
1145
+ let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema);
1146
+ if (match[1] === "SET") {
1147
+ restoredEntitiesResult.push(row);
1148
+ return;
1149
+ }
1150
+ removedIds.push(match[0]);
1151
+ });
1152
+ return [
1153
+ removedIds,
1154
+ restoredEntitiesResult
1155
+ ];
1156
+ };
1104
1157
  let writeBatchMethod = async (batch, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities, chainMetaData) => {
1105
1158
  let pgUpdates = [];
1106
1159
  let chUpdates = [];
@@ -1227,15 +1280,15 @@ export {
1227
1280
  setQueryCache,
1228
1281
  setOrThrow,
1229
1282
  makeSchemaTableNamesQuery,
1230
- cacheTablePrefixLength,
1231
1283
  makeSchemaCacheTableInfoQuery,
1232
1284
  getConnectedPsqlExec,
1233
1285
  deleteByIdsOrThrow,
1234
1286
  makeInsertDeleteUpdatesQuery,
1235
1287
  executeSet,
1236
1288
  writeBatch,
1237
- makeGetRollbackRestoredEntitiesQuery,
1289
+ makeGetRollbackPreTargetRowsQuery,
1238
1290
  makeGetRollbackRemovedIdsQuery,
1291
+ rollbackRowStateSchema,
1239
1292
  make,
1240
1293
  makeStorageFromEnv,
1241
1294
  makePersistenceFromConfig,