envio 3.0.0-rc.0 → 3.0.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 +18 -2
- package/package.json +6 -6
- package/src/Config.res +39 -6
- package/src/Config.res.mjs +44 -8
- package/src/Env.res +14 -1
- package/src/Env.res.mjs +24 -2
- package/src/Internal.res +9 -0
- package/src/PgStorage.res +27 -6
- package/src/PgStorage.res.mjs +22 -6
- package/src/Sink.res +2 -7
- package/src/Sink.res.mjs +4 -5
- package/src/TestIndexer.res +1 -1
- package/src/TestIndexer.res.mjs +1 -1
- package/src/UserContext.res +91 -56
- package/src/UserContext.res.mjs +39 -17
- package/src/bindings/ClickHouse.res +44 -11
- package/src/bindings/ClickHouse.res.mjs +34 -11
- package/src/tui/Tui.res +45 -4
- package/src/tui/Tui.res.mjs +42 -2
package/index.d.ts
CHANGED
|
@@ -1167,6 +1167,22 @@ type SvmEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
|
|
|
1167
1167
|
: never
|
|
1168
1168
|
: never;
|
|
1169
1169
|
|
|
1170
|
+
// Surfaced when the indexer has no configured ecosystem — typically because
|
|
1171
|
+
// `envio codegen` hasn't been run yet, so `Global` isn't augmented and
|
|
1172
|
+
// `GlobalConfig` resolves to `{}`. The handler methods are still present so
|
|
1173
|
+
// IDE autocomplete shows them; their rest parameter is typed as a
|
|
1174
|
+
// string-literal hint so any call site fails with an error message that
|
|
1175
|
+
// names codegen as the fix. A rest parameter (rather than a single one)
|
|
1176
|
+
// avoids "Expected N arguments" errors masking the hint.
|
|
1177
|
+
type CodegenRequiredHint =
|
|
1178
|
+
"Run 'envio codegen' to generate handler types from config.yaml. Without codegen, the indexer has no contracts, chains, or events to register handlers for.";
|
|
1179
|
+
type CodegenRequiredFallback = {
|
|
1180
|
+
readonly onEvent: (...hint: CodegenRequiredHint[]) => void;
|
|
1181
|
+
readonly onBlock: (...hint: CodegenRequiredHint[]) => void;
|
|
1182
|
+
readonly onSlot: (...hint: CodegenRequiredHint[]) => void;
|
|
1183
|
+
readonly contractRegister: (...hint: CodegenRequiredHint[]) => void;
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1170
1186
|
// Single-ecosystem chains live at the root of the indexer object alongside
|
|
1171
1187
|
// the handler-registration methods. Multi-ecosystem indexers aren't
|
|
1172
1188
|
// supported by the runtime, so there's no nested `evm` / `fuel` / `svm`
|
|
@@ -1178,7 +1194,7 @@ type SingleEcosystemChains<Config extends IndexerConfigTypes = GlobalConfig> =
|
|
|
1178
1194
|
? FuelEcosystem<Config>
|
|
1179
1195
|
: HasSvm<Config> extends true
|
|
1180
1196
|
? SvmEcosystem<Config>
|
|
1181
|
-
:
|
|
1197
|
+
: CodegenRequiredFallback;
|
|
1182
1198
|
|
|
1183
1199
|
/** Indexer type resolved from config. */
|
|
1184
1200
|
export type IndexerFromConfig<Config extends IndexerConfigTypes = GlobalConfig> = Prettify<
|
|
@@ -1457,7 +1473,7 @@ type SingleEcosystemTestChains<Config extends IndexerConfigTypes = GlobalConfig>
|
|
|
1457
1473
|
? FuelTestEcosystem<Config>
|
|
1458
1474
|
: HasSvm<Config> extends true
|
|
1459
1475
|
? SvmTestEcosystem<Config>
|
|
1460
|
-
:
|
|
1476
|
+
: CodegenRequiredFallback;
|
|
1461
1477
|
|
|
1462
1478
|
/**
|
|
1463
1479
|
* Test indexer type resolved from config.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envio",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
|
|
6
6
|
"bin": "./bin.mjs",
|
|
@@ -71,10 +71,10 @@
|
|
|
71
71
|
"tsx": "4.21.0"
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|
|
74
|
-
"envio-linux-x64": "3.0.0
|
|
75
|
-
"envio-linux-x64-musl": "3.0.0
|
|
76
|
-
"envio-linux-arm64": "3.0.0
|
|
77
|
-
"envio-darwin-x64": "3.0.0
|
|
78
|
-
"envio-darwin-arm64": "3.0.0
|
|
74
|
+
"envio-linux-x64": "3.0.0",
|
|
75
|
+
"envio-linux-x64-musl": "3.0.0",
|
|
76
|
+
"envio-linux-arm64": "3.0.0",
|
|
77
|
+
"envio-darwin-x64": "3.0.0",
|
|
78
|
+
"envio-darwin-arm64": "3.0.0"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/Config.res
CHANGED
|
@@ -145,11 +145,15 @@ module EnvioAddresses = {
|
|
|
145
145
|
external castToInternal: t => Internal.entity = "%identity"
|
|
146
146
|
|
|
147
147
|
let entityConfig = {
|
|
148
|
-
name,
|
|
148
|
+
Internal.name,
|
|
149
149
|
index,
|
|
150
150
|
schema,
|
|
151
151
|
rowsSchema,
|
|
152
152
|
table,
|
|
153
|
+
// Internal address tracking is Postgres-only; the global config is
|
|
154
|
+
// always required to have Postgres enabled (Storage::resolve forbids
|
|
155
|
+
// a Postgres-disabled global), so this is safe regardless of mode.
|
|
156
|
+
storage: {postgres: true, clickhouse: false},
|
|
153
157
|
}->Internal.fromGenericEntityConfig
|
|
154
158
|
}
|
|
155
159
|
|
|
@@ -269,9 +273,17 @@ let propertySchema = S.schema(s =>
|
|
|
269
273
|
}
|
|
270
274
|
)
|
|
271
275
|
|
|
276
|
+
let entityStorageSchema = S.schema(s =>
|
|
277
|
+
{
|
|
278
|
+
"postgres": s.matches(S.option(S.bool)),
|
|
279
|
+
"clickhouse": s.matches(S.option(S.bool)),
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
272
283
|
let entityJsonSchema = S.schema(s =>
|
|
273
284
|
{
|
|
274
285
|
"name": s.matches(S.string),
|
|
286
|
+
"storage": s.matches(S.option(entityStorageSchema)),
|
|
275
287
|
"properties": s.matches(S.array(propertySchema)),
|
|
276
288
|
"derivedFields": s.matches(S.option(S.array(derivedFieldSchema))),
|
|
277
289
|
"compositeIndices": s.matches(S.option(S.array(S.array(compositeIndexFieldSchema)))),
|
|
@@ -348,6 +360,7 @@ let parseEnumsFromJson = (enumsJson: dict<array<string>>): array<Table.enumConfi
|
|
|
348
360
|
let parseEntitiesFromJson = (
|
|
349
361
|
entitiesJson: array<'entityJson>,
|
|
350
362
|
~enumConfigsByName: dict<Table.enumConfig<Table.enum>>,
|
|
363
|
+
~globalStorage: storage,
|
|
351
364
|
): array<Internal.entityConfig> => {
|
|
352
365
|
entitiesJson->Array.mapWithIndex((entityJson, index) => {
|
|
353
366
|
let entityName = entityJson["name"]
|
|
@@ -416,6 +429,21 @@ let parseEntitiesFromJson = (
|
|
|
416
429
|
dict
|
|
417
430
|
})
|
|
418
431
|
|
|
432
|
+
// Resolve per-entity storage against the global config. The CLI
|
|
433
|
+
// validates that an entity never opts into a backend the global
|
|
434
|
+
// config didn't enable, and that at least one backend stays true
|
|
435
|
+
// for an annotated entity — so `getOr(false)` is safe here.
|
|
436
|
+
let storage: Internal.entityStorage = switch entityJson["storage"] {
|
|
437
|
+
| Some(s) => {
|
|
438
|
+
postgres: s["postgres"]->Option.getOr(false),
|
|
439
|
+
clickhouse: s["clickhouse"]->Option.getOr(false),
|
|
440
|
+
}
|
|
441
|
+
| None => {
|
|
442
|
+
postgres: globalStorage.postgres,
|
|
443
|
+
clickhouse: globalStorage.clickhouse,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
419
447
|
{
|
|
420
448
|
Internal.name: entityName,
|
|
421
449
|
index,
|
|
@@ -424,6 +452,7 @@ let parseEntitiesFromJson = (
|
|
|
424
452
|
Utils.magic: S.t<array<dict<unknown>>> => S.t<array<Internal.entity>>
|
|
425
453
|
),
|
|
426
454
|
table,
|
|
455
|
+
storage,
|
|
427
456
|
}->Internal.fromGenericEntityConfig
|
|
428
457
|
})
|
|
429
458
|
}
|
|
@@ -756,10 +785,15 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
756
785
|
let enumConfigsByName =
|
|
757
786
|
allEnums->Array.map(enumConfig => (enumConfig.name, enumConfig))->Dict.fromArray
|
|
758
787
|
|
|
788
|
+
let globalStorage: storage = {
|
|
789
|
+
postgres: publicConfig["storage"]["postgres"],
|
|
790
|
+
clickhouse: publicConfig["storage"]["clickhouse"]->Option.getOr(false),
|
|
791
|
+
}
|
|
792
|
+
|
|
759
793
|
let userEntities =
|
|
760
794
|
publicConfig["entities"]
|
|
761
795
|
->Option.getOr([])
|
|
762
|
-
->parseEntitiesFromJson(~enumConfigsByName)
|
|
796
|
+
->parseEntitiesFromJson(~enumConfigsByName, ~globalStorage)
|
|
763
797
|
|
|
764
798
|
let allEntities = userEntities->Array.concat([EnvioAddresses.entityConfig])
|
|
765
799
|
|
|
@@ -791,10 +825,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
791
825
|
contractHandlers,
|
|
792
826
|
shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
|
|
793
827
|
shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
|
|
794
|
-
storage:
|
|
795
|
-
postgres: publicConfig["storage"]["postgres"],
|
|
796
|
-
clickhouse: publicConfig["storage"]["clickhouse"]->Option.getOr(false),
|
|
797
|
-
},
|
|
828
|
+
storage: globalStorage,
|
|
798
829
|
multichain: publicConfig["multichain"]->Option.getOr(Unordered),
|
|
799
830
|
chainMap,
|
|
800
831
|
defaultChain: chains->Array.get(0),
|
|
@@ -992,3 +1023,5 @@ let loadWithoutRegistrations = () =>
|
|
|
992
1023
|
c
|
|
993
1024
|
}
|
|
994
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
let getPgUserEntities = (config: t) => config.userEntities->Array.filter(e => e.storage.postgres)
|
package/src/Config.res.mjs
CHANGED
|
@@ -51,12 +51,18 @@ let table = Table.mkTable(name, undefined, [
|
|
|
51
51
|
Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
+
let entityConfig_storage = {
|
|
55
|
+
postgres: true,
|
|
56
|
+
clickhouse: false
|
|
57
|
+
};
|
|
58
|
+
|
|
54
59
|
let entityConfig = {
|
|
55
60
|
name: name,
|
|
56
61
|
index: -1,
|
|
57
62
|
schema: schema,
|
|
58
63
|
rowsSchema: rowsSchema,
|
|
59
|
-
table: table
|
|
64
|
+
table: table,
|
|
65
|
+
storage: entityConfig_storage
|
|
60
66
|
};
|
|
61
67
|
|
|
62
68
|
let EnvioAddresses = {
|
|
@@ -168,8 +174,14 @@ let propertySchema = S$RescriptSchema.schema(s => ({
|
|
|
168
174
|
scale: s.m(S$RescriptSchema.option(S$RescriptSchema.int))
|
|
169
175
|
}));
|
|
170
176
|
|
|
177
|
+
let entityStorageSchema = S$RescriptSchema.schema(s => ({
|
|
178
|
+
postgres: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
|
|
179
|
+
clickhouse: s.m(S$RescriptSchema.option(S$RescriptSchema.bool))
|
|
180
|
+
}));
|
|
181
|
+
|
|
171
182
|
let entityJsonSchema = S$RescriptSchema.schema(s => ({
|
|
172
183
|
name: s.m(S$RescriptSchema.string),
|
|
184
|
+
storage: s.m(S$RescriptSchema.option(entityStorageSchema)),
|
|
173
185
|
properties: s.m(S$RescriptSchema.array(propertySchema)),
|
|
174
186
|
derivedFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(derivedFieldSchema))),
|
|
175
187
|
compositeIndices: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(compositeIndexFieldSchema))))
|
|
@@ -285,7 +297,7 @@ function parseEnumsFromJson(enumsJson) {
|
|
|
285
297
|
return Object.entries(enumsJson).map(param => Table.makeEnumConfig(param[0], param[1]));
|
|
286
298
|
}
|
|
287
299
|
|
|
288
|
-
function parseEntitiesFromJson(entitiesJson, enumConfigsByName) {
|
|
300
|
+
function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage) {
|
|
289
301
|
return entitiesJson.map((entityJson, index) => {
|
|
290
302
|
let entityName = entityJson.name;
|
|
291
303
|
let fields = entityJson.properties.map(prop => {
|
|
@@ -308,12 +320,27 @@ function parseEntitiesFromJson(entitiesJson, enumConfigsByName) {
|
|
|
308
320
|
});
|
|
309
321
|
return dict;
|
|
310
322
|
});
|
|
323
|
+
let s = entityJson.storage;
|
|
324
|
+
let storage;
|
|
325
|
+
if (s !== undefined) {
|
|
326
|
+
let s$1 = Primitive_option.valFromOption(s);
|
|
327
|
+
storage = {
|
|
328
|
+
postgres: Stdlib_Option.getOr(s$1.postgres, false),
|
|
329
|
+
clickhouse: Stdlib_Option.getOr(s$1.clickhouse, false)
|
|
330
|
+
};
|
|
331
|
+
} else {
|
|
332
|
+
storage = {
|
|
333
|
+
postgres: globalStorage.postgres,
|
|
334
|
+
clickhouse: globalStorage.clickhouse
|
|
335
|
+
};
|
|
336
|
+
}
|
|
311
337
|
return {
|
|
312
338
|
name: entityName,
|
|
313
339
|
index: index,
|
|
314
340
|
schema: schema,
|
|
315
341
|
rowsSchema: S$RescriptSchema.array(schema),
|
|
316
|
-
table: table
|
|
342
|
+
table: table,
|
|
343
|
+
storage: storage
|
|
317
344
|
};
|
|
318
345
|
});
|
|
319
346
|
}
|
|
@@ -583,7 +610,13 @@ function fromPublic(publicConfigJson) {
|
|
|
583
610
|
enumConfig.name,
|
|
584
611
|
enumConfig
|
|
585
612
|
]));
|
|
586
|
-
let
|
|
613
|
+
let globalStorage_postgres = publicConfig.storage.postgres;
|
|
614
|
+
let globalStorage_clickhouse = Stdlib_Option.getOr(publicConfig.storage.clickhouse, false);
|
|
615
|
+
let globalStorage = {
|
|
616
|
+
postgres: globalStorage_postgres,
|
|
617
|
+
clickhouse: globalStorage_clickhouse
|
|
618
|
+
};
|
|
619
|
+
let userEntities = parseEntitiesFromJson(Stdlib_Option.getOr(publicConfig.entities, []), enumConfigsByName, globalStorage);
|
|
587
620
|
let allEntities = userEntities.concat([entityConfig]);
|
|
588
621
|
let userEntitiesByName = Object.fromEntries(userEntities.map(entityConfig => [
|
|
589
622
|
entityConfig.name,
|
|
@@ -600,10 +633,7 @@ function fromPublic(publicConfigJson) {
|
|
|
600
633
|
contractHandlers: contractHandlers,
|
|
601
634
|
shouldRollbackOnReorg: Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true),
|
|
602
635
|
shouldSaveFullHistory: Stdlib_Option.getOr(publicConfig.saveFullHistory, false),
|
|
603
|
-
storage:
|
|
604
|
-
postgres: publicConfig.storage.postgres,
|
|
605
|
-
clickhouse: Stdlib_Option.getOr(publicConfig.storage.clickhouse, false)
|
|
606
|
-
},
|
|
636
|
+
storage: globalStorage,
|
|
607
637
|
multichain: Stdlib_Option.getOr(publicConfig.multichain, "unordered"),
|
|
608
638
|
chainMap: chainMap,
|
|
609
639
|
defaultChain: chains[0],
|
|
@@ -822,6 +852,10 @@ function loadWithoutRegistrations() {
|
|
|
822
852
|
return c$1;
|
|
823
853
|
}
|
|
824
854
|
|
|
855
|
+
function getPgUserEntities(config) {
|
|
856
|
+
return config.userEntities.filter(e => e.storage.postgres);
|
|
857
|
+
}
|
|
858
|
+
|
|
825
859
|
export {
|
|
826
860
|
EnvioAddresses,
|
|
827
861
|
rpcSourceForSchema,
|
|
@@ -836,6 +870,7 @@ export {
|
|
|
836
870
|
compositeIndexFieldSchema,
|
|
837
871
|
derivedFieldSchema,
|
|
838
872
|
propertySchema,
|
|
873
|
+
entityStorageSchema,
|
|
839
874
|
entityJsonSchema,
|
|
840
875
|
getFieldTypeAndSchema,
|
|
841
876
|
parseEnumsFromJson,
|
|
@@ -854,5 +889,6 @@ export {
|
|
|
854
889
|
diffPaths,
|
|
855
890
|
throwIfIncompatible,
|
|
856
891
|
loadWithoutRegistrations,
|
|
892
|
+
getPgUserEntities,
|
|
857
893
|
}
|
|
858
894
|
/* schema Not a pure module */
|
package/src/Env.res
CHANGED
|
@@ -134,11 +134,24 @@ module ClickHouse = {
|
|
|
134
134
|
const v = process.env[k];
|
|
135
135
|
return v === undefined || v === "" ? undefined : v;
|
|
136
136
|
}`)
|
|
137
|
+
// Empty password is a valid, passwordless ClickHouse user — distinguish
|
|
138
|
+
// "unset" (None) from "set but empty" (Some("")).
|
|
139
|
+
let readAllowEmpty: string => option<string> = %raw(`(k) => process.env[k]`)
|
|
137
140
|
)
|
|
138
141
|
let host = () => read("ENVIO_CLICKHOUSE_HOST")
|
|
139
142
|
let database = () => read("ENVIO_CLICKHOUSE_DATABASE")
|
|
140
143
|
let username = () => read("ENVIO_CLICKHOUSE_USERNAME")
|
|
141
|
-
let password = () =>
|
|
144
|
+
let password = () => readAllowEmpty("ENVIO_CLICKHOUSE_PASSWORD")
|
|
145
|
+
let replicated = () =>
|
|
146
|
+
switch read("ENVIO_CLICKHOUSE_REPLICATED") {
|
|
147
|
+
| None => false
|
|
148
|
+
| Some("true") => true
|
|
149
|
+
| Some(other) =>
|
|
150
|
+
JsError.throwWithMessage(
|
|
151
|
+
`Invalid ENVIO_CLICKHOUSE_REPLICATED value: "${other}". Only "true" is accepted.`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
let databaseEngine = () => read("ENVIO_CLICKHOUSE_DATABASE_ENGINE")
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
module Hasura = {
|
package/src/Env.res.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import * as EnvSafe from "./EnvSafe.res.mjs";
|
|
|
4
4
|
import * as Logging from "./Logging.res.mjs";
|
|
5
5
|
import * as Postgres from "./bindings/Postgres.res.mjs";
|
|
6
6
|
import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js";
|
|
7
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
8
|
import * as HyperSyncClient from "./sources/HyperSyncClient.res.mjs";
|
|
8
9
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
9
10
|
import * as HypersyncClient from "@envio-dev/hypersync-client";
|
|
@@ -107,6 +108,8 @@ let read = ((k) => {
|
|
|
107
108
|
return v === undefined || v === "" ? undefined : v;
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
let readAllowEmpty = ((k) => process.env[k]);
|
|
112
|
+
|
|
110
113
|
function host$1() {
|
|
111
114
|
return read("ENVIO_CLICKHOUSE_HOST");
|
|
112
115
|
}
|
|
@@ -120,14 +123,33 @@ function username() {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
function password$1() {
|
|
123
|
-
return
|
|
126
|
+
return readAllowEmpty("ENVIO_CLICKHOUSE_PASSWORD");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function replicated() {
|
|
130
|
+
let other = read("ENVIO_CLICKHOUSE_REPLICATED");
|
|
131
|
+
if (other !== undefined) {
|
|
132
|
+
if (other === "true") {
|
|
133
|
+
return true;
|
|
134
|
+
} else {
|
|
135
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ENVIO_CLICKHOUSE_REPLICATED value: "` + other + `". Only "true" is accepted.`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function databaseEngine() {
|
|
143
|
+
return read("ENVIO_CLICKHOUSE_DATABASE_ENGINE");
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
let ClickHouse = {
|
|
127
147
|
host: host$1,
|
|
128
148
|
database: database$1,
|
|
129
149
|
username: username,
|
|
130
|
-
password: password$1
|
|
150
|
+
password: password$1,
|
|
151
|
+
replicated: replicated,
|
|
152
|
+
databaseEngine: databaseEngine
|
|
131
153
|
};
|
|
132
154
|
|
|
133
155
|
let enabled = EnvSafe.get(envSafe, "ENVIO_HASURA", S$RescriptSchema.bool, undefined, true, undefined, undefined);
|
package/src/Internal.res
CHANGED
|
@@ -523,12 +523,21 @@ let fuelTransferParamsSchema = S.schema(s => {
|
|
|
523
523
|
type multichain = | @as("ordered") Ordered | @as("unordered") Unordered
|
|
524
524
|
|
|
525
525
|
type entity = private {id: string}
|
|
526
|
+
|
|
527
|
+
// Per-entity storage resolved at parse time against the global storage
|
|
528
|
+
// config. Downstream PG/CH consumers just check the matching boolean.
|
|
529
|
+
type entityStorage = {
|
|
530
|
+
postgres: bool,
|
|
531
|
+
clickhouse: bool,
|
|
532
|
+
}
|
|
533
|
+
|
|
526
534
|
type genericEntityConfig<'entity> = {
|
|
527
535
|
name: string,
|
|
528
536
|
index: int,
|
|
529
537
|
schema: S.t<'entity>,
|
|
530
538
|
rowsSchema: S.t<array<'entity>>,
|
|
531
539
|
table: Table.table,
|
|
540
|
+
storage: entityStorage,
|
|
532
541
|
}
|
|
533
542
|
type entityConfig = genericEntityConfig<entity>
|
|
534
543
|
external fromGenericEntityConfig: genericEntityConfig<'entity> => entityConfig = "%identity"
|
package/src/PgStorage.res
CHANGED
|
@@ -1208,6 +1208,12 @@ let make = (
|
|
|
1208
1208
|
~enums=[],
|
|
1209
1209
|
~envioInfo,
|
|
1210
1210
|
): Persistence.initialState => {
|
|
1211
|
+
// Per-entity storage routing: PG owns tables only for entities that
|
|
1212
|
+
// opted into Postgres; the sink mirrors only those that opted into
|
|
1213
|
+
// ClickHouse.
|
|
1214
|
+
let pgEntities = entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres)
|
|
1215
|
+
let chEntities = entities->Array.filter((e: Internal.entityConfig) => e.storage.clickhouse)
|
|
1216
|
+
|
|
1211
1217
|
let schemaTableNames: array<schemaTableName> = await sql->Postgres.unsafe(
|
|
1212
1218
|
makeSchemaTableNamesQuery(~pgSchema),
|
|
1213
1219
|
)
|
|
@@ -1235,14 +1241,14 @@ let make = (
|
|
|
1235
1241
|
|
|
1236
1242
|
// Call sink.initialize before executing PG queries
|
|
1237
1243
|
switch sink {
|
|
1238
|
-
| Some(sink) => await sink.initialize(~chainConfigs, ~entities, ~enums)
|
|
1244
|
+
| Some(sink) => await sink.initialize(~chainConfigs, ~entities=chEntities, ~enums)
|
|
1239
1245
|
| None => ()
|
|
1240
1246
|
}
|
|
1241
1247
|
|
|
1242
1248
|
let queries = makeInitializeTransaction(
|
|
1243
1249
|
~pgSchema,
|
|
1244
1250
|
~pgUser,
|
|
1245
|
-
~entities,
|
|
1251
|
+
~entities=pgEntities,
|
|
1246
1252
|
~enums,
|
|
1247
1253
|
~chainConfigs,
|
|
1248
1254
|
~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
|
|
@@ -1620,12 +1626,25 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1620
1626
|
~updatedEffectsCache,
|
|
1621
1627
|
~updatedEntities,
|
|
1622
1628
|
) => {
|
|
1629
|
+
let pgUpdates = []
|
|
1630
|
+
let chUpdates = []
|
|
1631
|
+
for i in 0 to updatedEntities->Array.length - 1 {
|
|
1632
|
+
let update = updatedEntities->Array.getUnsafe(i)
|
|
1633
|
+
let {entityConfig}: Persistence.updatedEntity = update
|
|
1634
|
+
if entityConfig.storage.postgres {
|
|
1635
|
+
pgUpdates->Array.push(update)
|
|
1636
|
+
}
|
|
1637
|
+
if entityConfig.storage.clickhouse {
|
|
1638
|
+
chUpdates->Array.push(update)
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1623
1642
|
// Initialize sink if configured
|
|
1624
1643
|
let sinkPromise = switch sink {
|
|
1625
1644
|
| Some(sink) => {
|
|
1626
1645
|
let timerRef = Hrtime.makeTimer()
|
|
1627
1646
|
Some(
|
|
1628
|
-
sink.writeBatch(~batch, ~updatedEntities)
|
|
1647
|
+
sink.writeBatch(~batch, ~updatedEntities=chUpdates)
|
|
1629
1648
|
->Promise.thenResolve(_ => {
|
|
1630
1649
|
Prometheus.StorageWrite.increment(
|
|
1631
1650
|
~storage=sink.name,
|
|
@@ -1652,7 +1671,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1652
1671
|
~allEntities,
|
|
1653
1672
|
~setEffectCacheOrThrow,
|
|
1654
1673
|
~updatedEffectsCache,
|
|
1655
|
-
~updatedEntities,
|
|
1674
|
+
~updatedEntities=pgUpdates,
|
|
1656
1675
|
~sinkPromise,
|
|
1657
1676
|
)
|
|
1658
1677
|
Prometheus.StorageWrite.increment(
|
|
@@ -1705,6 +1724,7 @@ let makeStorageFromEnv = (
|
|
|
1705
1724
|
let host = Env.ClickHouse.host()
|
|
1706
1725
|
let username = Env.ClickHouse.username()
|
|
1707
1726
|
let password = Env.ClickHouse.password()
|
|
1727
|
+
let database = Env.ClickHouse.database()
|
|
1708
1728
|
let missing = []
|
|
1709
1729
|
let checkEnv = (opt, name) =>
|
|
1710
1730
|
switch opt {
|
|
@@ -1714,6 +1734,7 @@ let makeStorageFromEnv = (
|
|
|
1714
1734
|
host->checkEnv("ENVIO_CLICKHOUSE_HOST")
|
|
1715
1735
|
username->checkEnv("ENVIO_CLICKHOUSE_USERNAME")
|
|
1716
1736
|
password->checkEnv("ENVIO_CLICKHOUSE_PASSWORD")
|
|
1737
|
+
database->checkEnv("ENVIO_CLICKHOUSE_DATABASE")
|
|
1717
1738
|
if missing->Array.length > 0 {
|
|
1718
1739
|
JsError.throwWithMessage(
|
|
1719
1740
|
`ClickHouse storage is enabled but required env vars are not set: ${missing->Array.joinUnsafe(
|
|
@@ -1724,7 +1745,7 @@ let makeStorageFromEnv = (
|
|
|
1724
1745
|
Some(
|
|
1725
1746
|
Sink.makeClickHouse(
|
|
1726
1747
|
~host=host->Option.getUnsafe,
|
|
1727
|
-
~database=
|
|
1748
|
+
~database=database->Option.getUnsafe,
|
|
1728
1749
|
~username=username->Option.getUnsafe,
|
|
1729
1750
|
~password=password->Option.getUnsafe,
|
|
1730
1751
|
),
|
|
@@ -1744,7 +1765,7 @@ let makeStorageFromEnv = (
|
|
|
1744
1765
|
secret: Env.Hasura.secret,
|
|
1745
1766
|
},
|
|
1746
1767
|
~pgSchema,
|
|
1747
|
-
~userEntities=config.
|
|
1768
|
+
~userEntities=config->Config.getPgUserEntities,
|
|
1748
1769
|
~responseLimit=Env.Hasura.responseLimit,
|
|
1749
1770
|
~schema=Schema.make(config.allEntities->Belt.Array.map(e => e.table)),
|
|
1750
1771
|
~aggregateEntities=Env.Hasura.aggregateEntities,
|
package/src/PgStorage.res.mjs
CHANGED
|
@@ -794,14 +794,16 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
794
794
|
let chainConfigs = chainConfigsOpt !== undefined ? chainConfigsOpt : [];
|
|
795
795
|
let entities = entitiesOpt !== undefined ? entitiesOpt : [];
|
|
796
796
|
let enums = enumsOpt !== undefined ? enumsOpt : [];
|
|
797
|
+
let pgEntities = entities.filter(e => e.storage.postgres);
|
|
798
|
+
let chEntities = entities.filter(e => e.storage.clickhouse);
|
|
797
799
|
let schemaTableNames = await sql.unsafe(makeSchemaTableNamesQuery(pgSchema));
|
|
798
800
|
if (Utils.$$Array.notEmpty(schemaTableNames) && !schemaTableNames.some(table => table.table_name === InternalTable.Chains.table.tableName ? true : table.table_name === "event_sync_state")) {
|
|
799
801
|
Stdlib_JsError.throwWithMessage(`Cannot run Envio migrations on PostgreSQL schema "` + pgSchema + `" because it contains non-Envio tables. Running migrations would delete all data in this schema.\n\nTo resolve this:\n1. If you want to use this schema, first backup any important data, then drop it with: "pnpm envio local db-migrate down"\n2. Or specify a different schema name by setting the "ENVIO_PG_SCHEMA" environment variable\n3. Or manually drop the schema in your database if you're certain the data is not needed.`);
|
|
800
802
|
}
|
|
801
803
|
if (sink !== undefined) {
|
|
802
|
-
await sink.initialize(chainConfigs,
|
|
804
|
+
await sink.initialize(chainConfigs, chEntities, enums);
|
|
803
805
|
}
|
|
804
|
-
let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs,
|
|
806
|
+
let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames));
|
|
805
807
|
await sql.begin(async sql => {
|
|
806
808
|
await Promise.all(queries.map(query => sql.unsafe(query)));
|
|
807
809
|
return await InternalTable.EnvioInfo.write(sql, pgSchema, envioInfo);
|
|
@@ -1019,17 +1021,29 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1019
1021
|
sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
|
|
1020
1022
|
]);
|
|
1021
1023
|
let writeBatchMethod = async (batch, rawEvents, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities) => {
|
|
1024
|
+
let pgUpdates = [];
|
|
1025
|
+
let chUpdates = [];
|
|
1026
|
+
for (let i = 0, i_finish = updatedEntities.length; i < i_finish; ++i) {
|
|
1027
|
+
let update = updatedEntities[i];
|
|
1028
|
+
let entityConfig = update.entityConfig;
|
|
1029
|
+
if (entityConfig.storage.postgres) {
|
|
1030
|
+
pgUpdates.push(update);
|
|
1031
|
+
}
|
|
1032
|
+
if (entityConfig.storage.clickhouse) {
|
|
1033
|
+
chUpdates.push(update);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1022
1036
|
let sinkPromise;
|
|
1023
1037
|
if (sink !== undefined) {
|
|
1024
1038
|
let timerRef = Hrtime.makeTimer();
|
|
1025
|
-
sinkPromise = sink.writeBatch(batch,
|
|
1039
|
+
sinkPromise = sink.writeBatch(batch, chUpdates).then(() => {
|
|
1026
1040
|
Prometheus.StorageWrite.increment(sink.name, Hrtime.toSecondsFloat(Hrtime.timeSince(timerRef)));
|
|
1027
1041
|
}).catch(exn => exn);
|
|
1028
1042
|
} else {
|
|
1029
1043
|
sinkPromise = undefined;
|
|
1030
1044
|
}
|
|
1031
1045
|
let primaryTimerRef = Hrtime.makeTimer();
|
|
1032
|
-
await writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache,
|
|
1046
|
+
await writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpointId, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, undefined);
|
|
1033
1047
|
return Prometheus.StorageWrite.increment("postgres", Hrtime.toSecondsFloat(Hrtime.timeSince(primaryTimerRef)));
|
|
1034
1048
|
};
|
|
1035
1049
|
let close = () => sql.end();
|
|
@@ -1062,6 +1076,7 @@ function makeStorageFromEnv(config, sqlOpt, pgSchemaOpt, isHasuraEnabledOpt) {
|
|
|
1062
1076
|
let host = Env.ClickHouse.host();
|
|
1063
1077
|
let username = Env.ClickHouse.username();
|
|
1064
1078
|
let password = Env.ClickHouse.password();
|
|
1079
|
+
let database = Env.ClickHouse.database();
|
|
1065
1080
|
let missing = [];
|
|
1066
1081
|
let checkEnv = (opt, name) => {
|
|
1067
1082
|
if (opt !== undefined) {
|
|
@@ -1074,17 +1089,18 @@ function makeStorageFromEnv(config, sqlOpt, pgSchemaOpt, isHasuraEnabledOpt) {
|
|
|
1074
1089
|
checkEnv(host, "ENVIO_CLICKHOUSE_HOST");
|
|
1075
1090
|
checkEnv(username, "ENVIO_CLICKHOUSE_USERNAME");
|
|
1076
1091
|
checkEnv(password, "ENVIO_CLICKHOUSE_PASSWORD");
|
|
1092
|
+
checkEnv(database, "ENVIO_CLICKHOUSE_DATABASE");
|
|
1077
1093
|
if (missing.length !== 0) {
|
|
1078
1094
|
Stdlib_JsError.throwWithMessage(`ClickHouse storage is enabled but required env vars are not set: ` + missing.join(", ") + `. Please set them, disable clickhouse in the \`storage\` config, or run \`envio dev\` for a pre-configured local ClickHouse.`);
|
|
1079
1095
|
}
|
|
1080
|
-
tmp = Sink.makeClickHouse(host,
|
|
1096
|
+
tmp = Sink.makeClickHouse(host, database, username, password);
|
|
1081
1097
|
} else {
|
|
1082
1098
|
tmp = undefined;
|
|
1083
1099
|
}
|
|
1084
1100
|
return make(sql, Env.Db.host, pgSchema, Env.Db.port, Env.Db.user, Env.Db.database, Env.Db.password, isHasuraEnabled, tmp, isHasuraEnabled ? () => Stdlib_Promise.$$catch(Hasura.trackDatabase(Env.Hasura.graphqlEndpoint, {
|
|
1085
1101
|
role: Env.Hasura.role,
|
|
1086
1102
|
secret: Env.Hasura.secret
|
|
1087
|
-
}, pgSchema, config
|
|
1103
|
+
}, pgSchema, Config.getPgUserEntities(config), Env.Hasura.aggregateEntities, Env.Hasura.responseLimit, Schema.make(Belt_Array.map(config.allEntities, e => e.table))), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking tables`))) : undefined, isHasuraEnabled ? tableNames => Stdlib_Promise.$$catch(Hasura.trackTables(Env.Hasura.graphqlEndpoint, {
|
|
1088
1104
|
role: Env.Hasura.role,
|
|
1089
1105
|
secret: Env.Hasura.secret
|
|
1090
1106
|
}, pgSchema, tableNames), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking new tables`))) : undefined);
|
package/src/Sink.res
CHANGED
|
@@ -19,13 +19,8 @@ let makeClickHouse = (~host, ~database, ~username, ~password): t => {
|
|
|
19
19
|
password,
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
// Don't
|
|
23
|
-
//
|
|
24
|
-
// Call USE database instead
|
|
25
|
-
let database = switch database {
|
|
26
|
-
| Some(database) => database
|
|
27
|
-
| None => "envio_sink"
|
|
28
|
-
}
|
|
22
|
+
// Don't pass database to the client; it would fail if the database doesn't
|
|
23
|
+
// exist yet. Each query qualifies the name explicitly or runs USE first.
|
|
29
24
|
|
|
30
25
|
let cache = Utils.WeakMap.make()
|
|
31
26
|
|
package/src/Sink.res.mjs
CHANGED
|
@@ -10,7 +10,6 @@ function makeClickHouse(host, database, username, password) {
|
|
|
10
10
|
username: username,
|
|
11
11
|
password: password
|
|
12
12
|
});
|
|
13
|
-
let database$1 = database !== undefined ? database : "envio_sink";
|
|
14
13
|
let cache = new WeakMap();
|
|
15
14
|
return {
|
|
16
15
|
name: "clickhouse",
|
|
@@ -18,12 +17,12 @@ function makeClickHouse(host, database, username, password) {
|
|
|
18
17
|
$staropt$star !== undefined;
|
|
19
18
|
let entities = $staropt$star$1 !== undefined ? $staropt$star$1 : [];
|
|
20
19
|
let enums = $staropt$star$2 !== undefined ? $staropt$star$2 : [];
|
|
21
|
-
return ClickHouse.initialize(client, database
|
|
20
|
+
return ClickHouse.initialize(client, database, entities, enums);
|
|
22
21
|
},
|
|
23
|
-
resume: checkpointId => ClickHouse.resume(client, database
|
|
22
|
+
resume: checkpointId => ClickHouse.resume(client, database, checkpointId),
|
|
24
23
|
writeBatch: async (batch, updatedEntities) => {
|
|
25
|
-
await Promise.all(Belt_Array.map(updatedEntities, param => ClickHouse.setUpdatesOrThrow(client, cache, param.updates, param.entityConfig, database
|
|
26
|
-
return await ClickHouse.setCheckpointsOrThrow(client, batch, database
|
|
24
|
+
await Promise.all(Belt_Array.map(updatedEntities, param => ClickHouse.setUpdatesOrThrow(client, cache, param.updates, param.entityConfig, database)));
|
|
25
|
+
return await ClickHouse.setCheckpointsOrThrow(client, batch, database);
|
|
27
26
|
}
|
|
28
27
|
};
|
|
29
28
|
}
|
package/src/TestIndexer.res
CHANGED
|
@@ -75,7 +75,7 @@ let handleLoadByField = (
|
|
|
75
75
|
let results = []
|
|
76
76
|
|
|
77
77
|
// Get the field schema from the entity's table to properly parse the JSON field value
|
|
78
|
-
let fieldSchema = switch entityConfig.table->Table.
|
|
78
|
+
let fieldSchema = switch entityConfig.table->Table.getFieldByDbName(fieldName) {
|
|
79
79
|
| Some(Table.Field({fieldSchema})) => fieldSchema
|
|
80
80
|
| _ => JsError.throwWithMessage(`Field ${fieldName} not found in entity ${tableName}`)
|
|
81
81
|
}
|
package/src/TestIndexer.res.mjs
CHANGED
|
@@ -51,7 +51,7 @@ function handleLoadByField(state, tableName, fieldName, fieldValue, operator) {
|
|
|
51
51
|
let entityDict = Stdlib_Option.getOr(state.entities[tableName], {});
|
|
52
52
|
let entityConfig = state.entityConfigs[tableName];
|
|
53
53
|
let results = [];
|
|
54
|
-
let match = Table.
|
|
54
|
+
let match = Table.getFieldByDbName(entityConfig.table, fieldName);
|
|
55
55
|
let fieldSchema;
|
|
56
56
|
let exit = 0;
|
|
57
57
|
if (match !== undefined && match.TAG === "Field") {
|
package/src/UserContext.res
CHANGED
|
@@ -203,10 +203,20 @@ let getWhereHandler = (params: entityContextParams, filter: dict<dict<unknown>>)
|
|
|
203
203
|
let noopSet = (_entity: Internal.entity) => ()
|
|
204
204
|
let noopDeleteUnsafe = (_entityId: string) => ()
|
|
205
205
|
|
|
206
|
+
// Reads against ClickHouse-only entities have no Postgres table to hit;
|
|
207
|
+
// surface a friendly error instead of letting the SQL layer fail with
|
|
208
|
+
// "relation does not exist".
|
|
209
|
+
let throwClickHouseReadOnly = (entityConfig: Internal.entityConfig, op: string) =>
|
|
210
|
+
JsError.throwWithMessage(
|
|
211
|
+
`context.${entityConfig.name}.${op}() is unavailable: ClickHouse storage is currently write-only. Follow Envio releases to be notified when ClickHouse supports both reads and writes from handlers.`,
|
|
212
|
+
)
|
|
213
|
+
|
|
206
214
|
let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
207
215
|
get: (~target as params, ~prop: unknown) => {
|
|
208
216
|
let prop = prop->(Utils.magic: unknown => string)
|
|
209
217
|
|
|
218
|
+
let isClickHouseOnly = !params.entityConfig.storage.postgres
|
|
219
|
+
|
|
210
220
|
let set = params.isPreload
|
|
211
221
|
? noopSet
|
|
212
222
|
: (entity: Internal.entity) => {
|
|
@@ -224,67 +234,92 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
224
234
|
|
|
225
235
|
switch prop {
|
|
226
236
|
| "get" =>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
if isClickHouseOnly {
|
|
238
|
+
((_entityId: string) => throwClickHouseReadOnly(params.entityConfig, "get"))->(
|
|
239
|
+
Utils.magic: (string => promise<option<Internal.entity>>) => unknown
|
|
240
|
+
)
|
|
241
|
+
} else {
|
|
242
|
+
(
|
|
243
|
+
entityId =>
|
|
244
|
+
LoadLayer.loadById(
|
|
245
|
+
~loadManager=params.loadManager,
|
|
246
|
+
~persistence=params.persistence,
|
|
247
|
+
~entityConfig=params.entityConfig,
|
|
248
|
+
~inMemoryStore=params.inMemoryStore,
|
|
249
|
+
~shouldGroup=params.isPreload,
|
|
250
|
+
~item=params.item,
|
|
251
|
+
~entityId,
|
|
252
|
+
)
|
|
253
|
+
)->(Utils.magic: (string => promise<option<Internal.entity>>) => unknown)
|
|
254
|
+
}
|
|
239
255
|
| "getWhere" =>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
if isClickHouseOnly {
|
|
257
|
+
((_filter: unknown) => throwClickHouseReadOnly(params.entityConfig, "getWhere"))->(
|
|
258
|
+
Utils.magic: (unknown => promise<array<Internal.entity>>) => unknown
|
|
259
|
+
)
|
|
260
|
+
} else {
|
|
261
|
+
(
|
|
262
|
+
filter => getWhereHandler(params, filter->(Utils.magic: unknown => dict<dict<unknown>>))
|
|
263
|
+
)->(Utils.magic: (unknown => promise<array<Internal.entity>>) => unknown)
|
|
264
|
+
}
|
|
243
265
|
|
|
244
266
|
| "getOrThrow" =>
|
|
245
|
-
|
|
246
|
-
(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
if isClickHouseOnly {
|
|
268
|
+
(
|
|
269
|
+
(_entityId: string, ~message as _=?) =>
|
|
270
|
+
throwClickHouseReadOnly(params.entityConfig, "getOrThrow")
|
|
271
|
+
)->(Utils.magic: ((string, ~message: string=?) => promise<Internal.entity>) => unknown)
|
|
272
|
+
} else {
|
|
273
|
+
(
|
|
274
|
+
(entityId, ~message=?) =>
|
|
275
|
+
LoadLayer.loadById(
|
|
276
|
+
~loadManager=params.loadManager,
|
|
277
|
+
~persistence=params.persistence,
|
|
278
|
+
~entityConfig=params.entityConfig,
|
|
279
|
+
~inMemoryStore=params.inMemoryStore,
|
|
280
|
+
~shouldGroup=params.isPreload,
|
|
281
|
+
~item=params.item,
|
|
282
|
+
~entityId,
|
|
283
|
+
)->Promise.thenResolve(entity => {
|
|
284
|
+
switch entity {
|
|
285
|
+
| Some(entity) => entity
|
|
286
|
+
| None =>
|
|
287
|
+
JsError.throwWithMessage(
|
|
288
|
+
message->Belt.Option.getWithDefault(
|
|
289
|
+
`Entity '${params.entityConfig.name}' with ID '${entityId}' is expected to exist.`,
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
)->(Utils.magic: ((string, ~message: string=?) => promise<Internal.entity>) => unknown)
|
|
295
|
+
}
|
|
267
296
|
| "getOrCreate" =>
|
|
268
|
-
|
|
269
|
-
(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
297
|
+
if isClickHouseOnly {
|
|
298
|
+
(
|
|
299
|
+
(_entity: Internal.entity) => throwClickHouseReadOnly(params.entityConfig, "getOrCreate")
|
|
300
|
+
)->(Utils.magic: (Internal.entity => promise<Internal.entity>) => unknown)
|
|
301
|
+
} else {
|
|
302
|
+
(
|
|
303
|
+
(entity: Internal.entity) =>
|
|
304
|
+
LoadLayer.loadById(
|
|
305
|
+
~loadManager=params.loadManager,
|
|
306
|
+
~persistence=params.persistence,
|
|
307
|
+
~entityConfig=params.entityConfig,
|
|
308
|
+
~inMemoryStore=params.inMemoryStore,
|
|
309
|
+
~shouldGroup=params.isPreload,
|
|
310
|
+
~item=params.item,
|
|
311
|
+
~entityId=entity.id,
|
|
312
|
+
)->Promise.thenResolve(storageEntity => {
|
|
313
|
+
switch storageEntity {
|
|
314
|
+
| Some(entity) => entity
|
|
315
|
+
| None => {
|
|
316
|
+
set(entity)
|
|
317
|
+
entity
|
|
318
|
+
}
|
|
284
319
|
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
320
|
+
})
|
|
321
|
+
)->(Utils.magic: (Internal.entity => promise<Internal.entity>) => unknown)
|
|
322
|
+
}
|
|
288
323
|
| "set" => set->(Utils.magic: (Internal.entity => unit) => unknown)
|
|
289
324
|
| "deleteUnsafe" =>
|
|
290
325
|
if params.isPreload {
|
package/src/UserContext.res.mjs
CHANGED
|
@@ -119,7 +119,12 @@ function noopDeleteUnsafe(_entityId) {
|
|
|
119
119
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function throwClickHouseReadOnly(entityConfig, op) {
|
|
123
|
+
return Stdlib_JsError.throwWithMessage(`context.` + entityConfig.name + `.` + op + `() is unavailable: ClickHouse storage is currently write-only. Follow Envio releases to be notified when ClickHouse supports both reads and writes from handlers.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
let entityTraps_get = (params, prop) => {
|
|
127
|
+
let isClickHouseOnly = !params.entityConfig.storage.postgres;
|
|
123
128
|
let set = params.isPreload ? noopSet : entity => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.inMemoryStore, params.entityConfig), {
|
|
124
129
|
type: "SET",
|
|
125
130
|
entityId: entity.id,
|
|
@@ -138,26 +143,42 @@ let entityTraps_get = (params, prop) => {
|
|
|
138
143
|
}, params.shouldSaveHistory, undefined);
|
|
139
144
|
}
|
|
140
145
|
case "get" :
|
|
141
|
-
|
|
146
|
+
if (isClickHouseOnly) {
|
|
147
|
+
return _entityId => throwClickHouseReadOnly(params.entityConfig, "get");
|
|
148
|
+
} else {
|
|
149
|
+
return entityId => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, params.inMemoryStore, params.isPreload, params.item, entityId);
|
|
150
|
+
}
|
|
142
151
|
case "getOrCreate" :
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
if (isClickHouseOnly) {
|
|
153
|
+
return _entity => throwClickHouseReadOnly(params.entityConfig, "getOrCreate");
|
|
154
|
+
} else {
|
|
155
|
+
return entity => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, params.inMemoryStore, params.isPreload, params.item, entity.id).then(storageEntity => {
|
|
156
|
+
if (storageEntity !== undefined) {
|
|
157
|
+
return storageEntity;
|
|
158
|
+
} else {
|
|
159
|
+
set(entity);
|
|
160
|
+
return entity;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
151
164
|
case "getOrThrow" :
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
if (isClickHouseOnly) {
|
|
166
|
+
return (_entityId, param) => throwClickHouseReadOnly(params.entityConfig, "getOrThrow");
|
|
167
|
+
} else {
|
|
168
|
+
return (entityId, message) => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, params.inMemoryStore, params.isPreload, params.item, entityId).then(entity => {
|
|
169
|
+
if (entity !== undefined) {
|
|
170
|
+
return entity;
|
|
171
|
+
} else {
|
|
172
|
+
return Stdlib_JsError.throwWithMessage(Belt_Option.getWithDefault(message, `Entity '` + params.entityConfig.name + `' with ID '` + entityId + `' is expected to exist.`));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
159
176
|
case "getWhere" :
|
|
160
|
-
|
|
177
|
+
if (isClickHouseOnly) {
|
|
178
|
+
return _filter => throwClickHouseReadOnly(params.entityConfig, "getWhere");
|
|
179
|
+
} else {
|
|
180
|
+
return filter => getWhereHandler(params, filter);
|
|
181
|
+
}
|
|
161
182
|
case "set" :
|
|
162
183
|
return set;
|
|
163
184
|
default:
|
|
@@ -284,6 +305,7 @@ export {
|
|
|
284
305
|
getWhereHandler,
|
|
285
306
|
noopSet,
|
|
286
307
|
noopDeleteUnsafe,
|
|
308
|
+
throwClickHouseReadOnly,
|
|
287
309
|
entityTraps,
|
|
288
310
|
handlerTraps,
|
|
289
311
|
getHandlerContext,
|
|
@@ -299,7 +299,12 @@ let setUpdatesOrThrow = async (
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
// Generate CREATE TABLE query for entity history table
|
|
302
|
-
let makeCreateHistoryTableQuery = (
|
|
302
|
+
let makeCreateHistoryTableQuery = (
|
|
303
|
+
~entityConfig: Internal.entityConfig,
|
|
304
|
+
~database: string,
|
|
305
|
+
~replicated: bool=false,
|
|
306
|
+
) => {
|
|
307
|
+
let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()"
|
|
303
308
|
let fieldDefinitions = entityConfig.table.fields->Belt.Array.keepMap(field => {
|
|
304
309
|
switch field {
|
|
305
310
|
| Field(field) =>
|
|
@@ -332,12 +337,13 @@ let makeCreateHistoryTableQuery = (~entityConfig: Internal.entityConfig, ~databa
|
|
|
332
337
|
~isArray=false,
|
|
333
338
|
)}
|
|
334
339
|
)
|
|
335
|
-
ENGINE =
|
|
340
|
+
ENGINE = ${tableEngine}
|
|
336
341
|
ORDER BY (${Table.idFieldName}, ${EntityHistory.checkpointIdFieldName})`
|
|
337
342
|
}
|
|
338
343
|
|
|
339
344
|
// Generate CREATE TABLE query for checkpoints
|
|
340
|
-
let makeCreateCheckpointsTableQuery = (~database: string) => {
|
|
345
|
+
let makeCreateCheckpointsTableQuery = (~database: string, ~replicated: bool=false) => {
|
|
346
|
+
let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()"
|
|
341
347
|
let idField = (#id: InternalTable.Checkpoints.field :> string)
|
|
342
348
|
let chainIdField = (#chain_id: InternalTable.Checkpoints.field :> string)
|
|
343
349
|
let blockNumberField = (#block_number: InternalTable.Checkpoints.field :> string)
|
|
@@ -367,7 +373,7 @@ let makeCreateCheckpointsTableQuery = (~database: string) => {
|
|
|
367
373
|
~isArray=false,
|
|
368
374
|
)}
|
|
369
375
|
)
|
|
370
|
-
ENGINE =
|
|
376
|
+
ENGINE = ${tableEngine}
|
|
371
377
|
ORDER BY (${idField})`
|
|
372
378
|
}
|
|
373
379
|
|
|
@@ -414,16 +420,43 @@ let initialize = async (
|
|
|
414
420
|
~enums as _: array<Table.enumConfig<Table.enum>>,
|
|
415
421
|
) => {
|
|
416
422
|
try {
|
|
423
|
+
let replicated = Env.ClickHouse.replicated()
|
|
424
|
+
let databaseEngine = Env.ClickHouse.databaseEngine()
|
|
425
|
+
let databaseEngineClause = switch databaseEngine {
|
|
426
|
+
| Some(engine) => ` ENGINE = ${engine}`
|
|
427
|
+
| None => ""
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
switch databaseEngine {
|
|
431
|
+
| Some(engineSpec) => {
|
|
432
|
+
let expectedEngineName = engineSpec->String.split("(")->Belt.Array.getUnsafe(0)->String.trim
|
|
433
|
+
let existingResult = await client->query({
|
|
434
|
+
query: `SELECT engine FROM system.databases WHERE name = '${database}'`,
|
|
435
|
+
})
|
|
436
|
+
let rows: array<{"engine": string}> = await existingResult->json
|
|
437
|
+
switch rows->Belt.Array.get(0) {
|
|
438
|
+
| Some(row) if row["engine"] !== expectedEngineName =>
|
|
439
|
+
JsError.throwWithMessage(
|
|
440
|
+
`ClickHouse database "${database}" exists with engine "${row["engine"]}" but ENVIO_CLICKHOUSE_DATABASE_ENGINE specifies "${expectedEngineName}". Drop the database manually to change its engine.`,
|
|
441
|
+
)
|
|
442
|
+
| _ => ()
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
| None => ()
|
|
446
|
+
}
|
|
447
|
+
|
|
417
448
|
await client->exec({query: `TRUNCATE DATABASE IF EXISTS ${database}`})
|
|
418
|
-
await client->exec({
|
|
449
|
+
await client->exec({
|
|
450
|
+
query: `CREATE DATABASE IF NOT EXISTS ${database}${databaseEngineClause}`,
|
|
451
|
+
})
|
|
419
452
|
await client->exec({query: `USE ${database}`})
|
|
420
453
|
|
|
421
454
|
await Promise.all(
|
|
422
455
|
entities->Belt.Array.map(entityConfig =>
|
|
423
|
-
client->exec({query: makeCreateHistoryTableQuery(~entityConfig, ~database)})
|
|
456
|
+
client->exec({query: makeCreateHistoryTableQuery(~entityConfig, ~database, ~replicated)})
|
|
424
457
|
),
|
|
425
458
|
)->Utils.Promise.ignoreValue
|
|
426
|
-
await client->exec({query: makeCreateCheckpointsTableQuery(~database)})
|
|
459
|
+
await client->exec({query: makeCreateCheckpointsTableQuery(~database, ~replicated)})
|
|
427
460
|
|
|
428
461
|
await Promise.all(
|
|
429
462
|
entities->Belt.Array.map(entityConfig =>
|
|
@@ -431,10 +464,10 @@ let initialize = async (
|
|
|
431
464
|
),
|
|
432
465
|
)->Utils.Promise.ignoreValue
|
|
433
466
|
|
|
434
|
-
Logging.trace("ClickHouse
|
|
467
|
+
Logging.trace("ClickHouse storage initialization completed successfully")
|
|
435
468
|
} catch {
|
|
436
469
|
| exn => {
|
|
437
|
-
Logging.errorWithExn(exn, "Failed to initialize ClickHouse
|
|
470
|
+
Logging.errorWithExn(exn, "Failed to initialize ClickHouse storage")
|
|
438
471
|
JsError.throwWithMessage("ClickHouse initialization failed")
|
|
439
472
|
}
|
|
440
473
|
}
|
|
@@ -450,7 +483,7 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
|
|
|
450
483
|
| exn =>
|
|
451
484
|
Logging.errorWithExn(
|
|
452
485
|
exn,
|
|
453
|
-
`ClickHouse
|
|
486
|
+
`ClickHouse storage database "${database}" not found. Please run 'envio start -r' to reinitialize the indexer (it'll also drop Postgres database).`,
|
|
454
487
|
)
|
|
455
488
|
JsError.throwWithMessage("ClickHouse resume failed")
|
|
456
489
|
}
|
|
@@ -478,7 +511,7 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
|
|
|
478
511
|
} catch {
|
|
479
512
|
| Persistence.StorageError(_) as exn => throw(exn)
|
|
480
513
|
| exn => {
|
|
481
|
-
Logging.errorWithExn(exn, "Failed to resume ClickHouse
|
|
514
|
+
Logging.errorWithExn(exn, "Failed to resume ClickHouse storage")
|
|
482
515
|
JsError.throwWithMessage("ClickHouse resume failed")
|
|
483
516
|
}
|
|
484
517
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Env from "../Env.res.mjs";
|
|
3
4
|
import * as Table from "../db/Table.res.mjs";
|
|
4
5
|
import * as Utils from "../Utils.res.mjs";
|
|
5
6
|
import * as Logging from "../Logging.res.mjs";
|
|
@@ -8,6 +9,7 @@ import * as Persistence from "../Persistence.res.mjs";
|
|
|
8
9
|
import * as EntityHistory from "../db/EntityHistory.res.mjs";
|
|
9
10
|
import * as InternalTable from "../db/InternalTable.res.mjs";
|
|
10
11
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
12
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
13
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
12
14
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
13
15
|
|
|
@@ -234,7 +236,9 @@ async function setUpdatesOrThrow(client, cache, updates, entityConfig, database)
|
|
|
234
236
|
}
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
function makeCreateHistoryTableQuery(entityConfig, database) {
|
|
239
|
+
function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt) {
|
|
240
|
+
let replicated = replicatedOpt !== undefined ? replicatedOpt : false;
|
|
241
|
+
let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()";
|
|
238
242
|
let fieldDefinitions = Belt_Array.keepMap(entityConfig.table.fields, field => {
|
|
239
243
|
if (field.TAG !== "Field") {
|
|
240
244
|
return;
|
|
@@ -252,11 +256,13 @@ function makeCreateHistoryTableQuery(entityConfig, database) {
|
|
|
252
256
|
config: EntityHistory.RowAction.config
|
|
253
257
|
}, false, false) + `
|
|
254
258
|
)
|
|
255
|
-
ENGINE =
|
|
259
|
+
ENGINE = ` + tableEngine + `
|
|
256
260
|
ORDER BY (` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`;
|
|
257
261
|
}
|
|
258
262
|
|
|
259
|
-
function makeCreateCheckpointsTableQuery(database) {
|
|
263
|
+
function makeCreateCheckpointsTableQuery(database, replicatedOpt) {
|
|
264
|
+
let replicated = replicatedOpt !== undefined ? replicatedOpt : false;
|
|
265
|
+
let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()";
|
|
260
266
|
return `CREATE TABLE IF NOT EXISTS ` + database + `.\`` + InternalTable.Checkpoints.table.tableName + `\` (
|
|
261
267
|
\`` + "id" + `\` ` + getClickHouseFieldType("UInt64", false, false) + `,
|
|
262
268
|
\`` + "chain_id" + `\` ` + getClickHouseFieldType("Int32", false, false) + `,
|
|
@@ -264,7 +270,7 @@ function makeCreateCheckpointsTableQuery(database) {
|
|
|
264
270
|
\`` + "block_hash" + `\` ` + getClickHouseFieldType("String", true, false) + `,
|
|
265
271
|
\`` + "events_processed" + `\` ` + getClickHouseFieldType("UInt64", false, false) + `
|
|
266
272
|
)
|
|
267
|
-
ENGINE =
|
|
273
|
+
ENGINE = ` + tableEngine + `
|
|
268
274
|
ORDER BY (` + "id" + `)`;
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -292,28 +298,45 @@ WHERE \`` + EntityHistory.changeFieldName + `\` = '` + "SET" + `'`;
|
|
|
292
298
|
|
|
293
299
|
async function initialize(client, database, entities, param) {
|
|
294
300
|
try {
|
|
301
|
+
let replicated = Env.ClickHouse.replicated();
|
|
302
|
+
let databaseEngine = Env.ClickHouse.databaseEngine();
|
|
303
|
+
let databaseEngineClause = databaseEngine !== undefined ? ` ENGINE = ` + databaseEngine : "";
|
|
304
|
+
if (databaseEngine !== undefined) {
|
|
305
|
+
let expectedEngineName = databaseEngine.split("(")[0].trim();
|
|
306
|
+
let existingResult = await client.query({
|
|
307
|
+
query: `SELECT engine FROM system.databases WHERE name = '` + database + `'`
|
|
308
|
+
});
|
|
309
|
+
let rows = await existingResult.json();
|
|
310
|
+
let row = Belt_Array.get(rows, 0);
|
|
311
|
+
if (row !== undefined) {
|
|
312
|
+
let row$1 = Primitive_option.valFromOption(row);
|
|
313
|
+
if (row$1.engine !== expectedEngineName) {
|
|
314
|
+
Stdlib_JsError.throwWithMessage(`ClickHouse database "` + database + `" exists with engine "` + row$1.engine + `" but ENVIO_CLICKHOUSE_DATABASE_ENGINE specifies "` + expectedEngineName + `". Drop the database manually to change its engine.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
295
318
|
await client.exec({
|
|
296
319
|
query: `TRUNCATE DATABASE IF EXISTS ` + database
|
|
297
320
|
});
|
|
298
321
|
await client.exec({
|
|
299
|
-
query: `CREATE DATABASE IF NOT EXISTS ` + database
|
|
322
|
+
query: `CREATE DATABASE IF NOT EXISTS ` + database + databaseEngineClause
|
|
300
323
|
});
|
|
301
324
|
await client.exec({
|
|
302
325
|
query: `USE ` + database
|
|
303
326
|
});
|
|
304
327
|
await Promise.all(Belt_Array.map(entities, entityConfig => client.exec({
|
|
305
|
-
query: makeCreateHistoryTableQuery(entityConfig, database)
|
|
328
|
+
query: makeCreateHistoryTableQuery(entityConfig, database, replicated)
|
|
306
329
|
})));
|
|
307
330
|
await client.exec({
|
|
308
|
-
query: makeCreateCheckpointsTableQuery(database)
|
|
331
|
+
query: makeCreateCheckpointsTableQuery(database, replicated)
|
|
309
332
|
});
|
|
310
333
|
await Promise.all(Belt_Array.map(entities, entityConfig => client.exec({
|
|
311
334
|
query: makeCreateViewQuery(entityConfig, database)
|
|
312
335
|
})));
|
|
313
|
-
return Logging.trace("ClickHouse
|
|
336
|
+
return Logging.trace("ClickHouse storage initialization completed successfully");
|
|
314
337
|
} catch (raw_exn) {
|
|
315
338
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
316
|
-
Logging.errorWithExn(exn, "Failed to initialize ClickHouse
|
|
339
|
+
Logging.errorWithExn(exn, "Failed to initialize ClickHouse storage");
|
|
317
340
|
return Stdlib_JsError.throwWithMessage("ClickHouse initialization failed");
|
|
318
341
|
}
|
|
319
342
|
}
|
|
@@ -326,7 +349,7 @@ async function resume(client, database, checkpointId) {
|
|
|
326
349
|
});
|
|
327
350
|
} catch (raw_exn) {
|
|
328
351
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
329
|
-
Logging.errorWithExn(exn, `ClickHouse
|
|
352
|
+
Logging.errorWithExn(exn, `ClickHouse storage database "` + database + `" not found. Please run 'envio start -r' to reinitialize the indexer (it'll also drop Postgres database).`);
|
|
330
353
|
Stdlib_JsError.throwWithMessage("ClickHouse resume failed");
|
|
331
354
|
}
|
|
332
355
|
let tablesResult = await client.query({
|
|
@@ -347,7 +370,7 @@ async function resume(client, database, checkpointId) {
|
|
|
347
370
|
if (exn$1.RE_EXN_ID === Persistence.StorageError) {
|
|
348
371
|
throw exn$1;
|
|
349
372
|
}
|
|
350
|
-
Logging.errorWithExn(exn$1, "Failed to resume ClickHouse
|
|
373
|
+
Logging.errorWithExn(exn$1, "Failed to resume ClickHouse storage");
|
|
351
374
|
return Stdlib_JsError.throwWithMessage("ClickHouse resume failed");
|
|
352
375
|
}
|
|
353
376
|
}
|
package/src/tui/Tui.res
CHANGED
|
@@ -78,15 +78,52 @@ module ChainLine = {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
module EventsPerSecond = {
|
|
82
|
+
type sample = {time: float, events: float}
|
|
83
|
+
|
|
84
|
+
let windowMs = 60_000.
|
|
85
|
+
|
|
86
|
+
let computeEps = (samples: array<sample>) => {
|
|
87
|
+
let len = samples->Array.length
|
|
88
|
+
switch (samples->Array.get(0), samples->Array.get(len - 1)) {
|
|
89
|
+
| (Some(first), Some(last)) if last.time > first.time =>
|
|
90
|
+
Some((last.events -. first.events) /. ((last.time -. first.time) /. 1000.))
|
|
91
|
+
| _ => None
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let use = (~totalEventsProcessed: float) => {
|
|
96
|
+
let (samples, setSamples) = React.useState((): array<sample> => [])
|
|
97
|
+
|
|
98
|
+
React.useEffect1(() => {
|
|
99
|
+
let now = Date.now()
|
|
100
|
+
let cutoff = now -. windowMs
|
|
101
|
+
setSamples(prev => {
|
|
102
|
+
let kept = prev->Array.filter(s => s.time >= cutoff)
|
|
103
|
+
kept->Array.concat([{time: now, events: totalEventsProcessed}])
|
|
104
|
+
})
|
|
105
|
+
None
|
|
106
|
+
}, [totalEventsProcessed])
|
|
107
|
+
|
|
108
|
+
computeEps(samples)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
81
112
|
module TotalEventsProcessed = {
|
|
82
113
|
@react.component
|
|
83
|
-
let make = (~totalEventsProcessed) => {
|
|
84
|
-
let label = "Total Events: "
|
|
114
|
+
let make = (~totalEventsProcessed, ~eventsPerSecond: option<float>) => {
|
|
85
115
|
<Text>
|
|
86
|
-
<Text bold=true> {
|
|
116
|
+
<Text bold=true> {"Total Events: "->React.string} </Text>
|
|
87
117
|
<Text color={Secondary}>
|
|
88
118
|
{`${totalEventsProcessed->TuiData.formatFloatLocaleString}`->React.string}
|
|
89
119
|
</Text>
|
|
120
|
+
{switch eventsPerSecond {
|
|
121
|
+
| Some(eps) =>
|
|
122
|
+
<Text color={Gray}>
|
|
123
|
+
{` (${Math.round(eps)->TuiData.formatFloatLocaleString} events/sec)`->React.string}
|
|
124
|
+
</Text>
|
|
125
|
+
| None => React.null
|
|
126
|
+
}}
|
|
90
127
|
</Text>
|
|
91
128
|
}
|
|
92
129
|
}
|
|
@@ -188,6 +225,7 @@ module App = {
|
|
|
188
225
|
acc
|
|
189
226
|
}
|
|
190
227
|
})
|
|
228
|
+
let eventsPerSecond = EventsPerSecond.use(~totalEventsProcessed)
|
|
191
229
|
|
|
192
230
|
<Box flexDirection={Column}>
|
|
193
231
|
<BigText
|
|
@@ -214,7 +252,10 @@ module App = {
|
|
|
214
252
|
/>
|
|
215
253
|
})
|
|
216
254
|
->React.array}
|
|
217
|
-
<TotalEventsProcessed
|
|
255
|
+
<TotalEventsProcessed
|
|
256
|
+
totalEventsProcessed
|
|
257
|
+
eventsPerSecond={SyncETA.isIndexerFullySynced(chains) ? None : eventsPerSecond}
|
|
258
|
+
/>
|
|
218
259
|
<SyncETA chains indexerStartTime=state.indexerStartTime />
|
|
219
260
|
<Newline />
|
|
220
261
|
<Box flexDirection={Row}>
|
package/src/tui/Tui.res.mjs
CHANGED
|
@@ -136,7 +136,40 @@ let ChainLine = {
|
|
|
136
136
|
make: Tui$ChainLine
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
+
function computeEps(samples) {
|
|
140
|
+
let len = samples.length;
|
|
141
|
+
let match = samples[0];
|
|
142
|
+
let match$1 = samples[len - 1 | 0];
|
|
143
|
+
if (match !== undefined && match$1 !== undefined && match$1.time > match.time) {
|
|
144
|
+
return (match$1.events - match.events) / ((match$1.time - match.time) / 1000);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function use(totalEventsProcessed) {
|
|
149
|
+
let match = React.useState(() => []);
|
|
150
|
+
let setSamples = match[1];
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
let now = Date.now();
|
|
153
|
+
let cutoff = now - 60000;
|
|
154
|
+
setSamples(prev => {
|
|
155
|
+
let kept = prev.filter(s => s.time >= cutoff);
|
|
156
|
+
return kept.concat([{
|
|
157
|
+
time: now,
|
|
158
|
+
events: totalEventsProcessed
|
|
159
|
+
}]);
|
|
160
|
+
});
|
|
161
|
+
}, [totalEventsProcessed]);
|
|
162
|
+
return computeEps(match[0]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let EventsPerSecond = {
|
|
166
|
+
windowMs: 60000,
|
|
167
|
+
computeEps: computeEps,
|
|
168
|
+
use: use
|
|
169
|
+
};
|
|
170
|
+
|
|
139
171
|
function Tui$TotalEventsProcessed(props) {
|
|
172
|
+
let eventsPerSecond = props.eventsPerSecond;
|
|
140
173
|
return JsxRuntime.jsxs(Ink$1.Text, {
|
|
141
174
|
children: [
|
|
142
175
|
JsxRuntime.jsx(Ink$1.Text, {
|
|
@@ -146,7 +179,11 @@ function Tui$TotalEventsProcessed(props) {
|
|
|
146
179
|
JsxRuntime.jsx(Ink$1.Text, {
|
|
147
180
|
children: TuiData.formatFloatLocaleString(props.totalEventsProcessed),
|
|
148
181
|
color: "#FFBB2F"
|
|
149
|
-
})
|
|
182
|
+
}),
|
|
183
|
+
eventsPerSecond !== undefined ? JsxRuntime.jsx(Ink$1.Text, {
|
|
184
|
+
children: ` (` + TuiData.formatFloatLocaleString(Math.round(eventsPerSecond)) + ` events/sec)`,
|
|
185
|
+
color: "gray"
|
|
186
|
+
}) : null
|
|
150
187
|
]
|
|
151
188
|
});
|
|
152
189
|
}
|
|
@@ -229,6 +266,7 @@ function Tui$App(props) {
|
|
|
229
266
|
return acc;
|
|
230
267
|
}
|
|
231
268
|
});
|
|
269
|
+
let eventsPerSecond = use(totalEventsProcessed);
|
|
232
270
|
let defaultPassword = "testing";
|
|
233
271
|
let match$1 = state.ctx.config.storage.clickhouse;
|
|
234
272
|
let match$2 = Env.ClickHouse.host();
|
|
@@ -257,7 +295,8 @@ function Tui$App(props) {
|
|
|
257
295
|
eventsProcessed: chainData.eventsProcessed
|
|
258
296
|
}, i.toString())),
|
|
259
297
|
JsxRuntime.jsx(Tui$TotalEventsProcessed, {
|
|
260
|
-
totalEventsProcessed: totalEventsProcessed
|
|
298
|
+
totalEventsProcessed: totalEventsProcessed,
|
|
299
|
+
eventsPerSecond: SyncETA.isIndexerFullySynced(chains) ? undefined : eventsPerSecond
|
|
261
300
|
}),
|
|
262
301
|
JsxRuntime.jsx(SyncETA.make, {
|
|
263
302
|
chains: chains,
|
|
@@ -331,6 +370,7 @@ function start(getState) {
|
|
|
331
370
|
|
|
332
371
|
export {
|
|
333
372
|
ChainLine,
|
|
373
|
+
EventsPerSecond,
|
|
334
374
|
TotalEventsProcessed,
|
|
335
375
|
App,
|
|
336
376
|
start,
|