envio 3.12.0 → 3.13.0-alpha.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/package.json +6 -6
- package/src/BatchProcessing.res +9 -9
- package/src/BatchProcessing.res.mjs +5 -5
- package/src/Bin.res +24 -17
- package/src/Bin.res.mjs +5 -0
- package/src/ChainFetching.res +31 -13
- package/src/ChainFetching.res.mjs +9 -4
- package/src/ChainState.res +55 -3
- package/src/ChainState.res.mjs +46 -3
- package/src/ChainState.resi +3 -1
- package/src/Config.res +35 -0
- package/src/Config.res.mjs +39 -0
- package/src/Core.res +4 -0
- package/src/Core.res.mjs +4 -0
- package/src/CrossChainState.res +61 -4
- package/src/CrossChainState.res.mjs +39 -5
- package/src/CrossChainState.resi +10 -1
- package/src/Env.res +4 -0
- package/src/FetchState.res +51 -46
- package/src/FetchState.res.mjs +54 -36
- package/src/InMemoryTable.res +155 -67
- package/src/InMemoryTable.res.mjs +151 -60
- package/src/IndexerLoop.res +2 -0
- package/src/IndexerLoop.res.mjs +1 -0
- package/src/IndexerState.res +41 -1
- package/src/IndexerState.res.mjs +43 -4
- package/src/IndexerState.resi +10 -0
- package/src/LoadLayer.res +57 -34
- package/src/LoadLayer.res.mjs +45 -62
- package/src/LoadLayer.resi +1 -1
- package/src/Logging.res +38 -6
- package/src/Logging.res.mjs +32 -5
- package/src/Main.res +131 -268
- package/src/Main.res.mjs +28 -152
- package/src/Metrics.res +263 -102
- package/src/Metrics.res.mjs +227 -48
- package/src/Persistence.res +27 -2
- package/src/Persistence.res.mjs +9 -2
- package/src/PgStorage.res +109 -71
- package/src/PgStorage.res.mjs +88 -58
- package/src/Server.res +181 -0
- package/src/Server.res.mjs +143 -0
- package/src/Supervisor.res +415 -0
- package/src/Supervisor.res.mjs +325 -0
- package/src/TestIndexer.res +9 -25
- package/src/TestIndexer.res.mjs +5 -4
- package/src/UserContext.res +13 -32
- package/src/UserContext.res.mjs +1 -7
- package/src/Utils.res +1 -4
- package/src/Utils.res.mjs +7 -16
- package/src/Worker.res +95 -0
- package/src/Worker.res.mjs +80 -0
- package/src/bindings/NodeJs.res +41 -0
- package/src/db/EntityFilter.res +487 -275
- package/src/db/EntityFilter.res.mjs +557 -309
- package/src/db/InternalTable.res +8 -1
- package/src/db/InternalTable.res.mjs +5 -1
- package/src/db/Table.res +21 -6
- package/src/db/Table.res.mjs +13 -4
- package/src/sources/BlockStore.res +7 -2
- package/src/sources/EvmHyperSyncSource.res +2 -0
- package/src/sources/EvmHyperSyncSource.res.mjs +2 -2
- package/src/sources/FuelHyperSyncSource.res +1 -0
- package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
- package/src/sources/HyperSync.res +4 -0
- package/src/sources/HyperSync.res.mjs +4 -2
- package/src/sources/HyperSync.resi +1 -0
- package/src/sources/HyperSyncClient.res +3 -0
- package/src/sources/HyperSyncSSE.res +1 -1
- package/src/sources/HyperSyncSSE.res.mjs +4 -10
- package/src/sources/RpcSource.res +1 -0
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/SimulateSource.res +1 -0
- package/src/sources/SimulateSource.res.mjs +1 -1
- package/src/sources/Source.res +7 -0
- package/src/sources/SourceManager.res +4 -3
- package/src/sources/SourceManager.res.mjs +2 -2
- package/src/sources/SvmHyperSyncClient.res +5 -0
- package/src/sources/SvmHyperSyncSource.res +3 -0
- package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
- package/src/tui/Tui.res +24 -0
- package/src/tui/Tui.res.mjs +18 -0
- package/src/tui/components/SyncETA.res +12 -6
- package/src/tui/components/SyncETA.res.mjs +12 -8
package/src/PgStorage.res
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
let makeClient = () => {
|
|
1
|
+
let makeClient = (~maxConnections=Env.Db.maxConnections) => {
|
|
2
2
|
Postgres.makeSql(
|
|
3
3
|
~config={
|
|
4
4
|
host: Env.Db.host,
|
|
@@ -14,7 +14,7 @@ let makeClient = () => {
|
|
|
14
14
|
: Some(_str => ())
|
|
15
15
|
),
|
|
16
16
|
transform: {undefined: Null},
|
|
17
|
-
max:
|
|
17
|
+
max: maxConnections,
|
|
18
18
|
// debug: (~connection, ~query, ~params as _, ~types as _) => Js.log2(connection, query),
|
|
19
19
|
},
|
|
20
20
|
)
|
|
@@ -409,9 +409,10 @@ let makeLoadQuery = (~pgSchema, ~tableName, ~condition) => {
|
|
|
409
409
|
// Field names are spliced as quoted identifiers only after the queryFields
|
|
410
410
|
// lookup proves they exist on the table (and they originate from
|
|
411
411
|
// codegen-validated schemas), so the interpolation can't be abused.
|
|
412
|
-
let
|
|
412
|
+
let makeFilterCondition = (
|
|
413
413
|
~filter: EntityFilter.t,
|
|
414
414
|
~table: Table.table,
|
|
415
|
+
~pgSchema,
|
|
415
416
|
~params: array<unknown>,
|
|
416
417
|
) => {
|
|
417
418
|
// Filters reference fields by API name, while the SQL references columns
|
|
@@ -447,60 +448,89 @@ let rec makeFilterCondition = (
|
|
|
447
448
|
params->Array.push(param)->ignore
|
|
448
449
|
`$${params->Array.length->Int.toString}`
|
|
449
450
|
}
|
|
450
|
-
|
|
451
|
+
|
|
452
|
+
let condition = ref("")
|
|
453
|
+
filter
|
|
454
|
+
->EntityFilter.entries
|
|
455
|
+
->Utils.Dict.forEachWithKey((operators, fieldName) => {
|
|
451
456
|
let queryField = getQueryFieldOrThrow(fieldName)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
457
|
+
operators->Utils.Dict.forEachWithKey((fieldValue, operator) => {
|
|
458
|
+
let column = `"${queryField.pgDbFieldName}"`
|
|
459
|
+
let part = switch operator {
|
|
460
|
+
// A per-chain entity's table is partitioned by its chain-id column, and
|
|
461
|
+
// Postgres can only prune a plan it caches when that column is a constant
|
|
462
|
+
// in the SQL. Bound, the cached plan has to keep every partition, and the
|
|
463
|
+
// planner ends up throwing it away and re-planning on every execution
|
|
464
|
+
// instead — measured at 315us per load against 218us with the id written
|
|
465
|
+
// in, on 30 chains.
|
|
466
|
+
//
|
|
467
|
+
// The cost is that each chain gets its own query text, so Postgres caches
|
|
468
|
+
// a prepared statement per (entity, chain, filter shape) rather than per
|
|
469
|
+
// (entity, filter shape). Measured at ~8KB of plan cache each, which is
|
|
470
|
+
// ~10MB per connection for 40 entities across 30 chains — accepted, since
|
|
471
|
+
// the alternative is a cached plan that can't prune.
|
|
472
|
+
//
|
|
473
|
+
// `EntityFilter.scoped` is what puts this filter here, and the value is
|
|
474
|
+
// range-checked to a non-negative safe integer, so it can carry nothing
|
|
475
|
+
// but digits.
|
|
476
|
+
| "_eq" if queryField.isChainId =>
|
|
477
|
+
`${column} = ${fieldValue->ChainId.normalizeOrThrow->ChainId.toString}`
|
|
478
|
+
// Postgres arrays are rectangular, so candidates for a list column can't
|
|
479
|
+
// be bound as one array unless they all have the same length, and
|
|
480
|
+
// postgres.js can't bind a boolean array at all
|
|
481
|
+
// (https://github.com/porsager/postgres/issues/471). One equality per
|
|
482
|
+
// candidate has neither problem.
|
|
483
|
+
| "_in" if queryField.isArray || queryField.fieldType === Boolean =>
|
|
484
|
+
switch fieldValue->EntityFilter.asArray {
|
|
485
|
+
| [] => "FALSE"
|
|
486
|
+
| candidates =>
|
|
487
|
+
`(${candidates
|
|
488
|
+
->Array.map(
|
|
489
|
+
candidate =>
|
|
490
|
+
`${column} = ${serializeParamOrThrow(
|
|
491
|
+
~queryField,
|
|
492
|
+
~fieldName,
|
|
493
|
+
~fieldValue=candidate,
|
|
494
|
+
~isArray=false,
|
|
495
|
+
)}`,
|
|
496
|
+
)
|
|
497
|
+
->Array.join(" OR ")})`
|
|
498
|
+
}
|
|
499
|
+
| "_in" =>
|
|
500
|
+
let param = serializeParamOrThrow(~queryField, ~fieldName, ~fieldValue, ~isArray=true)
|
|
501
|
+
switch queryField.fieldType {
|
|
502
|
+
// A bound array of strings is text[], which has no equality with an
|
|
503
|
+
// enum. The insert casts the same way.
|
|
504
|
+
| Enum({config}) => `${column} = ANY(${param}::TEXT[]::"${pgSchema}".${config.name}[])`
|
|
505
|
+
| _ => `${column} = ANY(${param})`
|
|
506
|
+
}
|
|
507
|
+
| _ =>
|
|
508
|
+
let sqlOperator = switch operator {
|
|
509
|
+
| "_eq" => "="
|
|
510
|
+
| "_gt" => ">"
|
|
511
|
+
| "_lt" => "<"
|
|
512
|
+
| "_gte" => ">="
|
|
513
|
+
| "_lte" => "<="
|
|
514
|
+
| _ =>
|
|
515
|
+
throw(
|
|
516
|
+
Persistence.StorageError({
|
|
517
|
+
message: `Failed loading "${table.tableName}" from storage. Unknown filter operator "${operator}".`,
|
|
518
|
+
reason: Utils.Error.make(`Unknown filter operator "${operator}"`),
|
|
519
|
+
}),
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
`${column} ${sqlOperator} ${serializeParamOrThrow(
|
|
523
|
+
~queryField,
|
|
524
|
+
~fieldName,
|
|
525
|
+
~fieldValue,
|
|
526
|
+
~isArray=false,
|
|
527
|
+
)}`
|
|
528
|
+
}
|
|
529
|
+
condition := (condition.contents === "" ? part : condition.contents ++ " AND " ++ part)
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
condition.contents
|
|
504
534
|
}
|
|
505
535
|
|
|
506
536
|
// The chain-id predicate a per-chain entity's row-level SQL needs, already
|
|
@@ -1709,7 +1739,7 @@ let make = (
|
|
|
1709
1739
|
// Must match PG_CONTAINER in packages/cli/src/docker_env.rs
|
|
1710
1740
|
let containerName = "envio-postgres"
|
|
1711
1741
|
let psqlExecOptions: NodeJs.ChildProcess.execOptions = {
|
|
1712
|
-
env:
|
|
1742
|
+
env: dict{"PGPASSWORD": pgPassword, "PATH": %raw(`process.env.PATH`)},
|
|
1713
1743
|
}
|
|
1714
1744
|
|
|
1715
1745
|
let cacheDirPath = NodeJs.Path.resolve([
|
|
@@ -2002,7 +2032,7 @@ let make = (
|
|
|
2002
2032
|
|
|
2003
2033
|
let loadOrThrow = async (~filter: EntityFilter.t, ~table: Table.table) => {
|
|
2004
2034
|
let params = []
|
|
2005
|
-
let condition = makeFilterCondition(~filter, ~table, ~params)
|
|
2035
|
+
let condition = makeFilterCondition(~filter, ~table, ~pgSchema, ~params)
|
|
2006
2036
|
switch await sql->Postgres.preparedUnsafe(
|
|
2007
2037
|
makeLoadQuery(~pgSchema, ~tableName=table.tableName, ~condition),
|
|
2008
2038
|
params->Obj.magic,
|
|
@@ -2033,9 +2063,10 @@ let make = (
|
|
|
2033
2063
|
let queryFields = table->Table.queryFields
|
|
2034
2064
|
let columns = []
|
|
2035
2065
|
let seen = Utils.Set.make()
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2066
|
+
filters->Array.forEach(filter =>
|
|
2067
|
+
filter
|
|
2068
|
+
->EntityFilter.entries
|
|
2069
|
+
->Utils.Dict.forEachWithKey((_, fieldName) =>
|
|
2039
2070
|
switch queryFields->Utils.Dict.dangerouslyGetNonOption(fieldName) {
|
|
2040
2071
|
| Some({pgDbFieldName}) =>
|
|
2041
2072
|
if !(seen->Utils.Set.has(pgDbFieldName)) {
|
|
@@ -2044,9 +2075,8 @@ let make = (
|
|
|
2044
2075
|
}
|
|
2045
2076
|
| None => ()
|
|
2046
2077
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
filters->Array.forEach(collect)
|
|
2078
|
+
)
|
|
2079
|
+
)
|
|
2050
2080
|
columns
|
|
2051
2081
|
}
|
|
2052
2082
|
|
|
@@ -2234,13 +2264,17 @@ let make = (
|
|
|
2234
2264
|
}
|
|
2235
2265
|
|
|
2236
2266
|
switch missing {
|
|
2237
|
-
|
|
2267
|
+
// A schema that declares no indexes has nothing to say about them, and one
|
|
2268
|
+
// whose indexes are all in place says it once. Either way the line that
|
|
2269
|
+
// matters is the indexer reporting itself ready, which finalization logs.
|
|
2270
|
+
| [] if schemaIndexes->Utils.Array.notEmpty =>
|
|
2238
2271
|
Logging.info({
|
|
2239
2272
|
"storage": storageName,
|
|
2240
2273
|
"msg": `All ${schemaIndexes
|
|
2241
2274
|
->Array.length
|
|
2242
2275
|
->Int.toString} schema indexes are already in place. Marking the indexer ready.`,
|
|
2243
2276
|
})
|
|
2277
|
+
| [] => ()
|
|
2244
2278
|
| _ =>
|
|
2245
2279
|
Logging.info({
|
|
2246
2280
|
"storage": storageName,
|
|
@@ -2288,12 +2322,16 @@ let make = (
|
|
|
2288
2322
|
}
|
|
2289
2323
|
})
|
|
2290
2324
|
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2325
|
+
// Only when something was built: the wait this closes is the index build,
|
|
2326
|
+
// and the stamp on its own is not one anybody waited through.
|
|
2327
|
+
if missing->Utils.Array.notEmpty {
|
|
2328
|
+
Logging.info({
|
|
2329
|
+
"storage": storageName,
|
|
2330
|
+
"msg": `Committed ${missing
|
|
2331
|
+
->Array.length
|
|
2332
|
+
->Int.toString} schema indexes and the ready timestamp in ${timeRef->formatSeconds}s.`,
|
|
2333
|
+
})
|
|
2334
|
+
}
|
|
2297
2335
|
}
|
|
2298
2336
|
|
|
2299
2337
|
let setOrThrow = (
|
package/src/PgStorage.res.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import * as ChainState from "./ChainState.res.mjs";
|
|
|
19
19
|
import * as AddressRows from "./AddressRows.res.mjs";
|
|
20
20
|
import * as Performance from "./bindings/Performance.res.mjs";
|
|
21
21
|
import * as Persistence from "./Persistence.res.mjs";
|
|
22
|
+
import * as EntityFilter from "./db/EntityFilter.res.mjs";
|
|
22
23
|
import * as IndexCatalog from "./db/IndexCatalog.res.mjs";
|
|
23
24
|
import * as IndexManager from "./db/IndexManager.res.mjs";
|
|
24
25
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
@@ -37,7 +38,8 @@ import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
|
37
38
|
import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
|
|
38
39
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
39
40
|
|
|
40
|
-
function makeClient() {
|
|
41
|
+
function makeClient(maxConnectionsOpt) {
|
|
42
|
+
let maxConnections = maxConnectionsOpt !== undefined ? maxConnectionsOpt : Env.Db.maxConnections;
|
|
41
43
|
return Postgres({
|
|
42
44
|
host: Env.Db.host,
|
|
43
45
|
port: Env.Db.port,
|
|
@@ -45,7 +47,7 @@ function makeClient() {
|
|
|
45
47
|
username: Env.Db.user,
|
|
46
48
|
password: Env.Db.password,
|
|
47
49
|
ssl: Env.Db.ssl,
|
|
48
|
-
max:
|
|
50
|
+
max: maxConnections,
|
|
49
51
|
onnotice: Primitive_object.equal(Env.userLogLevel, "warn") || Primitive_object.equal(Env.userLogLevel, "error") ? undefined : _str => {},
|
|
50
52
|
transform: {
|
|
51
53
|
undefined: null
|
|
@@ -301,7 +303,7 @@ function makeLoadQuery(pgSchema, tableName, condition) {
|
|
|
301
303
|
return `SELECT * FROM "` + pgSchema + `"."` + tableName + `" WHERE ` + condition + `;`;
|
|
302
304
|
}
|
|
303
305
|
|
|
304
|
-
function makeFilterCondition(filter, table, params) {
|
|
306
|
+
function makeFilterCondition(filter, table, pgSchema, params) {
|
|
305
307
|
let getQueryFieldOrThrow = fieldName => {
|
|
306
308
|
let queryField = Table.queryFields(table)[fieldName];
|
|
307
309
|
if (queryField !== undefined) {
|
|
@@ -333,39 +335,76 @@ function makeFilterCondition(filter, table, params) {
|
|
|
333
335
|
params.push(param);
|
|
334
336
|
return `$` + params.length.toString();
|
|
335
337
|
};
|
|
336
|
-
let
|
|
337
|
-
|
|
338
|
-
return `"` + queryField.pgDbFieldName + `" ` + op + ` ` + serializeParamOrThrow(queryField, fieldName, fieldValue, false);
|
|
338
|
+
let condition = {
|
|
339
|
+
contents: ""
|
|
339
340
|
};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
let
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
Utils.Dict.forEachWithKey(EntityFilter.entries(filter), (operators, fieldName) => {
|
|
342
|
+
let queryField = getQueryFieldOrThrow(fieldName);
|
|
343
|
+
Utils.Dict.forEachWithKey(operators, (fieldValue, operator) => {
|
|
344
|
+
let column = `"` + queryField.pgDbFieldName + `"`;
|
|
345
|
+
let part;
|
|
346
|
+
let exit = 0;
|
|
347
|
+
switch (operator) {
|
|
348
|
+
case "_eq" :
|
|
349
|
+
if (queryField.isChainId) {
|
|
350
|
+
part = column + ` = ` + ChainId.toString(ChainId.normalizeOrThrow(fieldValue));
|
|
351
|
+
} else {
|
|
352
|
+
exit = 1;
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
case "_in" :
|
|
356
|
+
if (queryField.isArray || queryField.fieldType === "Boolean") {
|
|
357
|
+
let candidates = EntityFilter.asArray(fieldValue);
|
|
358
|
+
part = candidates.length !== 0 ? `(` + candidates.map(candidate => column + ` = ` + serializeParamOrThrow(queryField, fieldName, candidate, false)).join(" OR ") + `)` : "FALSE";
|
|
359
|
+
} else {
|
|
360
|
+
let param = serializeParamOrThrow(queryField, fieldName, fieldValue, true);
|
|
361
|
+
let match = queryField.fieldType;
|
|
362
|
+
let exit$1 = 0;
|
|
363
|
+
if (typeof match !== "object" || match.type !== "Enum") {
|
|
364
|
+
exit$1 = 2;
|
|
365
|
+
} else {
|
|
366
|
+
part = column + ` = ANY(` + param + `::TEXT[]::"` + pgSchema + `".` + match.config.name + `[])`;
|
|
367
|
+
}
|
|
368
|
+
if (exit$1 === 2) {
|
|
369
|
+
part = column + ` = ANY(` + param + `)`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
default:
|
|
374
|
+
exit = 1;
|
|
348
375
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
376
|
+
if (exit === 1) {
|
|
377
|
+
let sqlOperator;
|
|
378
|
+
switch (operator) {
|
|
379
|
+
case "_eq" :
|
|
380
|
+
sqlOperator = "=";
|
|
381
|
+
break;
|
|
382
|
+
case "_gt" :
|
|
383
|
+
sqlOperator = ">";
|
|
384
|
+
break;
|
|
385
|
+
case "_gte" :
|
|
386
|
+
sqlOperator = ">=";
|
|
387
|
+
break;
|
|
388
|
+
case "_lt" :
|
|
389
|
+
sqlOperator = "<";
|
|
390
|
+
break;
|
|
391
|
+
case "_lte" :
|
|
392
|
+
sqlOperator = "<=";
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
throw {
|
|
396
|
+
RE_EXN_ID: Persistence.StorageError,
|
|
397
|
+
message: `Failed loading "` + table.tableName + `" from storage. Unknown filter operator "` + operator + `".`,
|
|
398
|
+
reason: new Error(`Unknown filter operator "` + operator + `"`),
|
|
399
|
+
Error: new Error()
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
part = column + ` ` + sqlOperator + ` ` + serializeParamOrThrow(queryField, fieldName, fieldValue, false);
|
|
361
403
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
Error: new Error()
|
|
367
|
-
};
|
|
368
|
-
}
|
|
404
|
+
condition.contents = condition.contents === "" ? part : condition.contents + " AND " + part;
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
return condition.contents;
|
|
369
408
|
}
|
|
370
409
|
|
|
371
410
|
function makeChainIdCondition(table, chainId) {
|
|
@@ -1032,16 +1071,10 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1032
1071
|
let chainIdMode = chainIdModeOpt !== undefined ? chainIdModeOpt : "int32";
|
|
1033
1072
|
let isolated = isolatedOpt !== undefined ? isolatedOpt : false;
|
|
1034
1073
|
let containerName = "envio-postgres";
|
|
1035
|
-
let psqlExecOptions_env =
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
],
|
|
1040
|
-
[
|
|
1041
|
-
"PATH",
|
|
1042
|
-
process.env.PATH
|
|
1043
|
-
]
|
|
1044
|
-
]);
|
|
1074
|
+
let psqlExecOptions_env = {
|
|
1075
|
+
PGPASSWORD: pgPassword,
|
|
1076
|
+
PATH: process.env.PATH
|
|
1077
|
+
};
|
|
1045
1078
|
let psqlExecOptions = {
|
|
1046
1079
|
env: psqlExecOptions_env
|
|
1047
1080
|
};
|
|
@@ -1222,7 +1255,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1222
1255
|
};
|
|
1223
1256
|
let loadOrThrow = async (filter, table) => {
|
|
1224
1257
|
let params = [];
|
|
1225
|
-
let condition = makeFilterCondition(filter, table, params);
|
|
1258
|
+
let condition = makeFilterCondition(filter, table, pgSchema, params);
|
|
1226
1259
|
let rows;
|
|
1227
1260
|
try {
|
|
1228
1261
|
rows = await sql.unsafe(makeLoadQuery(pgSchema, table.tableName, condition), params, {prepare: true});
|
|
@@ -1251,12 +1284,8 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1251
1284
|
let queryFields = Table.queryFields(table);
|
|
1252
1285
|
let columns = [];
|
|
1253
1286
|
let seen = new Set();
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
filter.filters.forEach(collect);
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
let match = queryFields[filter.fieldName];
|
|
1287
|
+
filters.forEach(filter => Utils.Dict.forEachWithKey(EntityFilter.entries(filter), (param, fieldName) => {
|
|
1288
|
+
let match = queryFields[fieldName];
|
|
1260
1289
|
if (match === undefined) {
|
|
1261
1290
|
return;
|
|
1262
1291
|
}
|
|
@@ -1266,8 +1295,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1266
1295
|
columns.push(pgDbFieldName);
|
|
1267
1296
|
return;
|
|
1268
1297
|
}
|
|
1269
|
-
};
|
|
1270
|
-
filters.forEach(collect);
|
|
1298
|
+
}));
|
|
1271
1299
|
return columns;
|
|
1272
1300
|
};
|
|
1273
1301
|
let runAndVerify = async (sql, prepared) => {
|
|
@@ -1380,7 +1408,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1380
1408
|
msg: `Creating the ` + missing.length.toString() + ` remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ` + slowOnLargeDatabaseNotice,
|
|
1381
1409
|
indexes: missing.map(prepared => prepared.name)
|
|
1382
1410
|
});
|
|
1383
|
-
} else {
|
|
1411
|
+
} else if (Utils.$$Array.notEmpty(schemaIndexes)) {
|
|
1384
1412
|
Logging.info({
|
|
1385
1413
|
storage: storageName,
|
|
1386
1414
|
msg: `All ` + schemaIndexes.length.toString() + ` schema indexes are already in place. Marking the indexer ready.`
|
|
@@ -1411,10 +1439,12 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1411
1439
|
], {prepare: true});
|
|
1412
1440
|
}
|
|
1413
1441
|
});
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1442
|
+
if (Utils.$$Array.notEmpty(missing)) {
|
|
1443
|
+
return Logging.info({
|
|
1444
|
+
storage: storageName,
|
|
1445
|
+
msg: `Committed ` + missing.length.toString() + ` schema indexes and the ready timestamp in ` + formatSeconds(timeRef) + `s.`
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1418
1448
|
};
|
|
1419
1449
|
let setOrThrow$1 = (items, table, itemSchema) => setOrThrow(sql, items, table, itemSchema, pgSchema, setQueryCache, chainIdMode);
|
|
1420
1450
|
let setEffectCacheOrThrow = async (table, itemSchema, items, initialize) => {
|
|
@@ -1636,7 +1666,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
1636
1666
|
}
|
|
1637
1667
|
|
|
1638
1668
|
function makeStorageFromEnv(config, sqlOpt, pgSchemaOpt, isHasuraEnabledOpt) {
|
|
1639
|
-
let sql = sqlOpt !== undefined ? Primitive_option.valFromOption(sqlOpt) : makeClient();
|
|
1669
|
+
let sql = sqlOpt !== undefined ? Primitive_option.valFromOption(sqlOpt) : makeClient(undefined);
|
|
1640
1670
|
let pgSchema = pgSchemaOpt !== undefined ? pgSchemaOpt : Env.Db.publicSchema;
|
|
1641
1671
|
let isHasuraEnabled = isHasuraEnabledOpt !== undefined ? isHasuraEnabledOpt : Env.Hasura.enabled;
|
|
1642
1672
|
let tmp;
|
package/src/Server.res
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The indexer's own HTTP surface: metrics for a scraper, health for an
|
|
2
|
+
// orchestrator, and the console's view of the run. What it serves is handed to
|
|
3
|
+
// it, so one process's readings and a supervised group's merged ones render the
|
|
4
|
+
// same way.
|
|
5
|
+
|
|
6
|
+
// The public console/state chain shape. Kept to exactly this field set for
|
|
7
|
+
// backward compatibility with consumers like RACE — new metric fields stay off
|
|
8
|
+
// the HTTP response.
|
|
9
|
+
type chainData = {
|
|
10
|
+
chainId: ChainId.t,
|
|
11
|
+
poweredByHyperSync: bool,
|
|
12
|
+
firstEventBlockNumber: option<int>,
|
|
13
|
+
latestProcessedBlock: option<int>,
|
|
14
|
+
timestampCaughtUpToHeadOrEndblock: option<Date.t>,
|
|
15
|
+
numEventsProcessed: float,
|
|
16
|
+
latestFetchedBlockNumber: int,
|
|
17
|
+
// Need this for API backwards compatibility
|
|
18
|
+
@as("currentBlockHeight")
|
|
19
|
+
knownHeight: int,
|
|
20
|
+
numBatchesFetched: int,
|
|
21
|
+
startBlock: int,
|
|
22
|
+
endBlock: option<int>,
|
|
23
|
+
numAddresses: int,
|
|
24
|
+
}
|
|
25
|
+
@tag("status")
|
|
26
|
+
type state =
|
|
27
|
+
| @as("disabled") Disabled({})
|
|
28
|
+
| @as("initializing") Initializing({})
|
|
29
|
+
| @as("active")
|
|
30
|
+
Active({
|
|
31
|
+
envioVersion: string,
|
|
32
|
+
chains: array<chainData>,
|
|
33
|
+
indexerStartTime: Date.t,
|
|
34
|
+
isPreRegisteringDynamicContracts: bool,
|
|
35
|
+
rollbackOnReorg: bool,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
let toChainData = (m: Metrics.chainMetrics): chainData => {
|
|
39
|
+
chainId: m.chainId,
|
|
40
|
+
poweredByHyperSync: m.poweredByHyperSync,
|
|
41
|
+
firstEventBlockNumber: m.firstEventBlockNumber,
|
|
42
|
+
latestProcessedBlock: m.latestProcessedBlock,
|
|
43
|
+
timestampCaughtUpToHeadOrEndblock: m.timestampCaughtUpToHeadOrEndblock,
|
|
44
|
+
numEventsProcessed: m.numEventsProcessed,
|
|
45
|
+
latestFetchedBlockNumber: m.latestFetchedBlockNumber,
|
|
46
|
+
knownHeight: m.knownHeight,
|
|
47
|
+
numBatchesFetched: m.numBatchesFetched,
|
|
48
|
+
startBlock: m.startBlock,
|
|
49
|
+
endBlock: m.endBlock,
|
|
50
|
+
numAddresses: m.numAddresses,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let chainDataSchema = S.schema((s): chainData => {
|
|
54
|
+
chainId: s.matches(ChainId.schema),
|
|
55
|
+
poweredByHyperSync: s.matches(S.bool),
|
|
56
|
+
firstEventBlockNumber: s.matches(S.option(S.int)),
|
|
57
|
+
latestProcessedBlock: s.matches(S.option(S.int)),
|
|
58
|
+
timestampCaughtUpToHeadOrEndblock: s.matches(S.option(S.datetime(S.string))),
|
|
59
|
+
numEventsProcessed: s.matches(S.float),
|
|
60
|
+
latestFetchedBlockNumber: s.matches(S.int),
|
|
61
|
+
knownHeight: s.matches(S.int),
|
|
62
|
+
numBatchesFetched: s.matches(S.int),
|
|
63
|
+
startBlock: s.matches(S.int),
|
|
64
|
+
endBlock: s.matches(S.option(S.int)),
|
|
65
|
+
numAddresses: s.matches(S.int),
|
|
66
|
+
})
|
|
67
|
+
let stateSchema = S.union([
|
|
68
|
+
S.literal(Disabled({})),
|
|
69
|
+
S.literal(Initializing({})),
|
|
70
|
+
S.schema(s => Active({
|
|
71
|
+
envioVersion: s.matches(S.string),
|
|
72
|
+
chains: s.matches(S.array(chainDataSchema)),
|
|
73
|
+
indexerStartTime: s.matches(S.datetime(S.string)),
|
|
74
|
+
// Keep the field, since Dev Console expects it to be present
|
|
75
|
+
isPreRegisteringDynamicContracts: false,
|
|
76
|
+
rollbackOnReorg: s.matches(S.bool),
|
|
77
|
+
})),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
// Runtime state lives in the process-wide `EnvioGlobal` record (shared
|
|
81
|
+
// across duplicate envio module instances); the slots are opaque there, so
|
|
82
|
+
// cast them to the real types here.
|
|
83
|
+
let startServer = (
|
|
84
|
+
~getMetrics: unit => option<Metrics.t>,
|
|
85
|
+
~envioVersion: string,
|
|
86
|
+
~onSyncCache: unit => promise<unit>,
|
|
87
|
+
~collectRuntime: unit => string,
|
|
88
|
+
~isDevelopmentMode: bool,
|
|
89
|
+
) => {
|
|
90
|
+
open Express
|
|
91
|
+
|
|
92
|
+
let app = make()
|
|
93
|
+
|
|
94
|
+
let consoleCorsMiddleware = (req, res, next) => {
|
|
95
|
+
switch req.headers->Dict.get("origin") {
|
|
96
|
+
| Some(origin) if origin === Env.prodEnvioAppUrl || origin === Env.envioAppUrl =>
|
|
97
|
+
res->setHeader("Access-Control-Allow-Origin", origin)
|
|
98
|
+
| _ => ()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res->setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
102
|
+
res->setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
|
|
103
|
+
|
|
104
|
+
if req.method === Rest.Options {
|
|
105
|
+
res->sendStatus(200)
|
|
106
|
+
} else {
|
|
107
|
+
next()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
app->useFor("/console", consoleCorsMiddleware)
|
|
111
|
+
app->useFor("/metrics", consoleCorsMiddleware)
|
|
112
|
+
app->useFor("/metrics/runtime", consoleCorsMiddleware)
|
|
113
|
+
|
|
114
|
+
app->get("/healthz", (_req, res) => {
|
|
115
|
+
// this is the machine readable port used in kubernetes to check the health of this service.
|
|
116
|
+
// aditional health information could be added in the future (info about errors, back-offs, etc).
|
|
117
|
+
res->sendStatus(200)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
app->get("/console/state", (_req, res) => {
|
|
121
|
+
let state = if !isDevelopmentMode {
|
|
122
|
+
Disabled({})
|
|
123
|
+
} else {
|
|
124
|
+
switch getMetrics() {
|
|
125
|
+
| None => Initializing({})
|
|
126
|
+
| Some(metrics) =>
|
|
127
|
+
Active({
|
|
128
|
+
envioVersion,
|
|
129
|
+
chains: metrics.chains->Array.map(toChainData),
|
|
130
|
+
indexerStartTime: metrics.startTime,
|
|
131
|
+
isPreRegisteringDynamicContracts: false,
|
|
132
|
+
rollbackOnReorg: metrics.rollbackEnabled,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
app->post("/console/syncCache", (_req, res) => {
|
|
141
|
+
if isDevelopmentMode {
|
|
142
|
+
onSyncCache()
|
|
143
|
+
->Promise.thenResolve(() => res->json(Boolean(true)))
|
|
144
|
+
// A dump that couldn't be made, or couldn't be confirmed, answers the
|
|
145
|
+
// same `false` a disabled console does. Leaving it unanswered would hold
|
|
146
|
+
// the request open for as long as the indexer runs.
|
|
147
|
+
->Promise.catch(exn => {
|
|
148
|
+
Logging.errorWithExn(exn, "Failed to sync the effect cache")
|
|
149
|
+
res->json(Boolean(false))
|
|
150
|
+
Promise.resolve()
|
|
151
|
+
})
|
|
152
|
+
->Promise.ignore
|
|
153
|
+
} else {
|
|
154
|
+
res->json(Boolean(false))
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
app->get("/metrics", (_req, res) => {
|
|
159
|
+
res->set("Content-Type", Metrics.contentType)
|
|
160
|
+
let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
app->get("/metrics/runtime", (_req, res) => {
|
|
164
|
+
res->set("Content-Type", Metrics.contentType)
|
|
165
|
+
let _ = res->endWithData(collectRuntime())
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
let server = app->listen(Env.serverPort)
|
|
169
|
+
server->Express.onError(err => {
|
|
170
|
+
let code = (err->(Utils.magic: JsExn.t => {..}))["code"]
|
|
171
|
+
if code === "EADDRINUSE" {
|
|
172
|
+
Logging.error(
|
|
173
|
+
`Port ${Env.serverPort->Int.toString} is already in use. To fix this either:` ++
|
|
174
|
+
`\n 1. Kill the process using the port: lsof -ti :${Env.serverPort->Int.toString} | xargs kill -9` ++ `\n 2. Use a different port by setting the ENVIO_INDEXER_PORT environment variable: ENVIO_INDEXER_PORT=9899 envio start`,
|
|
175
|
+
)
|
|
176
|
+
} else {
|
|
177
|
+
Logging.errorWithExn(err, "Failed to start indexer server")
|
|
178
|
+
}
|
|
179
|
+
NodeJs.process->NodeJs.exitWithCode(Failure)
|
|
180
|
+
})
|
|
181
|
+
}
|