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.
- package/index.d.ts +19 -0
- package/licenses/CLA.md +35 -0
- package/licenses/EULA.md +67 -0
- package/licenses/LICENSE.md +45 -0
- package/licenses/README.md +35 -0
- package/package.json +9 -8
- package/src/Batch.res.mjs +1 -1
- package/src/BatchProcessing.res +0 -7
- package/src/BatchProcessing.res.mjs +1 -8
- package/src/ChainFetching.res +30 -18
- package/src/ChainFetching.res.mjs +23 -13
- package/src/ChainState.res +134 -55
- package/src/ChainState.res.mjs +78 -36
- package/src/ChainState.resi +14 -2
- package/src/Config.res +9 -5
- package/src/Config.res.mjs +2 -2
- package/src/Core.res +20 -0
- package/src/Core.res.mjs +12 -0
- package/src/CrossChainState.res +115 -34
- package/src/CrossChainState.res.mjs +37 -14
- package/src/EffectState.res +100 -0
- package/src/EffectState.res.mjs +74 -0
- package/src/EffectState.resi +32 -0
- package/src/Envio.res +25 -7
- package/src/Envio.res.mjs +15 -19
- package/src/EventConfigBuilder.res +21 -46
- package/src/EventConfigBuilder.res.mjs +18 -25
- package/src/EventProcessing.res +8 -5
- package/src/EventProcessing.res.mjs +2 -1
- package/src/FetchState.res +814 -467
- package/src/FetchState.res.mjs +541 -369
- package/src/HandlerRegister.res +3 -1
- package/src/HandlerRegister.res.mjs +3 -2
- package/src/InMemoryStore.res +14 -26
- package/src/InMemoryStore.res.mjs +6 -19
- package/src/IndexerState.res +15 -39
- package/src/IndexerState.res.mjs +18 -29
- package/src/IndexerState.resi +2 -10
- package/src/Internal.res +98 -14
- package/src/Internal.res.mjs +96 -2
- package/src/LoadLayer.res +44 -24
- package/src/LoadLayer.res.mjs +43 -20
- package/src/LoadLayer.resi +1 -0
- package/src/LogSelection.res +0 -62
- package/src/LogSelection.res.mjs +0 -74
- package/src/Metrics.res +1 -1
- package/src/Metrics.res.mjs +1 -1
- package/src/Persistence.res +12 -3
- package/src/PgStorage.res +155 -82
- package/src/PgStorage.res.mjs +130 -77
- package/src/Prometheus.res +20 -10
- package/src/Prometheus.res.mjs +22 -10
- package/src/PruneStaleHistory.res +146 -35
- package/src/PruneStaleHistory.res.mjs +114 -20
- package/src/RawEvent.res +2 -2
- package/src/Rollback.res +32 -13
- package/src/Rollback.res.mjs +24 -11
- package/src/SimulateDeadInputTracker.res +1 -1
- package/src/SimulateItems.res +38 -6
- package/src/SimulateItems.res.mjs +28 -9
- package/src/TestIndexer.res +2 -2
- package/src/TestIndexer.res.mjs +2 -2
- package/src/TopicFilter.res +1 -25
- package/src/TopicFilter.res.mjs +0 -74
- package/src/UserContext.res +62 -23
- package/src/UserContext.res.mjs +26 -6
- package/src/Writing.res +60 -21
- package/src/Writing.res.mjs +27 -9
- package/src/bindings/NodeJs.res +5 -0
- package/src/bindings/Viem.res +0 -5
- package/src/sources/EventRouter.res +0 -21
- package/src/sources/EventRouter.res.mjs +0 -12
- package/src/sources/EvmChain.res +2 -27
- package/src/sources/EvmChain.res.mjs +2 -23
- package/src/sources/EvmRpcClient.res +58 -23
- package/src/sources/EvmRpcClient.res.mjs +11 -5
- package/src/sources/HyperSync.res +9 -38
- package/src/sources/HyperSync.res.mjs +16 -28
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +88 -11
- package/src/sources/HyperSyncClient.res.mjs +39 -6
- package/src/sources/HyperSyncSource.res +18 -199
- package/src/sources/HyperSyncSource.res.mjs +9 -128
- package/src/sources/Rpc.res +0 -32
- package/src/sources/Rpc.res.mjs +1 -46
- package/src/sources/RpcSource.res +129 -522
- package/src/sources/RpcSource.res.mjs +161 -366
- package/src/sources/SimulateSource.res +37 -19
- package/src/sources/SimulateSource.res.mjs +27 -10
- package/src/sources/SourceManager.res +3 -7
- package/src/sources/SourceManager.res.mjs +2 -7
- package/src/sources/SourceManager.resi +0 -2
- package/src/sources/SvmHyperSyncSource.res +13 -3
- 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
|
-
`
|
|
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
|
|
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((
|
|
1115
|
-
|
|
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
|
|
1161
|
-
//
|
|
1162
|
-
let
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
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
|
-
~
|
|
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
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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
|
-
|
|
1597
|
+
let command = `${psqlExec} -c 'COPY "${pgSchema}"."${tableName}" TO STDOUT WITH (FORMAT text, HEADER);' > ${outputFile}`
|
|
1539
1598
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
|
1735
|
+
// Get the latest pre-target row, including its SET or DELETE action.
|
|
1675
1736
|
sql
|
|
1676
1737
|
->Postgres.preparedUnsafe(
|
|
1677
|
-
|
|
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 (
|
package/src/PgStorage.res.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
ORDER BY ` + Table.idFieldName +
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
let
|
|
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
|
-
|
|
883
|
-
exit = 1;
|
|
918
|
+
Logging.error(`Failed to upload cache, continuing without it. ` + psqlExec._0);
|
|
884
919
|
}
|
|
885
920
|
} else {
|
|
886
|
-
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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) =>
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1289
|
+
makeGetRollbackPreTargetRowsQuery,
|
|
1238
1290
|
makeGetRollbackRemovedIdsQuery,
|
|
1291
|
+
rollbackRowStateSchema,
|
|
1239
1292
|
make,
|
|
1240
1293
|
makeStorageFromEnv,
|
|
1241
1294
|
makePersistenceFromConfig,
|