envio 3.13.0 → 3.14.0-subgraph

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/bin.mjs +10 -1
  2. package/index.d.ts +11 -0
  3. package/package.json +8 -20
  4. package/rescript.json +1 -4
  5. package/src/Config.res +78 -100
  6. package/src/Config.res.mjs +55 -64
  7. package/src/Core.res +67 -6
  8. package/src/Core.res.mjs +58 -4
  9. package/src/EventConfigBuilder.res +4 -10
  10. package/src/EventConfigBuilder.res.mjs +1 -3
  11. package/src/EventProcessing.res +6 -6
  12. package/src/EventProcessing.res.mjs +8 -6
  13. package/src/ExitOnCaughtUp.res +5 -1
  14. package/src/ExitOnCaughtUp.res.mjs +8 -3
  15. package/src/HandlerLoader.res +98 -36
  16. package/src/HandlerLoader.res.mjs +89 -24
  17. package/src/HandlerTypeCheck.mjs +14 -0
  18. package/src/HandlerTypeCheckWorker.mjs +68 -0
  19. package/src/Internal.res +1 -1
  20. package/src/LoadLayer.res +39 -7
  21. package/src/LoadLayer.res.mjs +65 -38
  22. package/src/LoadLayer.resi +28 -0
  23. package/src/Main.res +17 -12
  24. package/src/Main.res.mjs +9 -3
  25. package/src/NodeVersion.mjs +13 -0
  26. package/src/Persistence.res +79 -38
  27. package/src/Persistence.res.mjs +41 -10
  28. package/src/PgStorage.res +86 -39
  29. package/src/PgStorage.res.mjs +83 -32
  30. package/src/ResumePlan.res +172 -0
  31. package/src/ResumePlan.res.mjs +122 -0
  32. package/src/Supervisor.res +8 -8
  33. package/src/Supervisor.res.mjs +6 -1
  34. package/src/SyncWrites.res +86 -0
  35. package/src/SyncWrites.res.mjs +85 -0
  36. package/src/TestIndexer.res +14 -4
  37. package/src/TestIndexer.res.mjs +3 -2
  38. package/src/TsModuleHooks.mjs +121 -0
  39. package/src/UserContext.res +377 -62
  40. package/src/UserContext.res.mjs +297 -39
  41. package/src/Utils.res +0 -8
  42. package/src/Worker.res +8 -0
  43. package/src/Worker.res.mjs +11 -0
  44. package/src/bindings/NodeJs.res +8 -7
  45. package/src/bindings/NodeJs.res.mjs +0 -3
  46. package/src/bindings/Pino.res +2 -2
  47. package/src/db/InternalTable.res +52 -2
  48. package/src/db/InternalTable.res.mjs +32 -3
  49. package/src/db/Table.res +10 -0
  50. package/src/db/Table.res.mjs +22 -11
  51. package/src/sources/FuelHyperSync.res +2 -0
  52. package/src/sources/FuelHyperSync.res.mjs +1 -0
  53. package/src/sources/FuelHyperSync.resi +1 -0
  54. package/src/sources/FuelHyperSyncClient.res +25 -8
  55. package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
  56. package/src/sources/FuelHyperSyncSource.res +24 -22
  57. package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
  58. package/src/subgraph/abi-types.ts +63 -0
  59. package/src/subgraph/assemblyscript.ts +85 -0
  60. package/src/subgraph/blocks.ts +183 -0
  61. package/src/subgraph/calls.ts +149 -0
  62. package/src/subgraph/conformance.ts +83 -0
  63. package/src/subgraph/errors.ts +96 -0
  64. package/src/subgraph/graph-cli-worker.mjs +26 -0
  65. package/src/subgraph/graph-cli.ts +190 -0
  66. package/src/subgraph/graph-ts-enums.ts +28 -0
  67. package/src/subgraph/graph-ts-types/VERSION +2 -0
  68. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  69. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  70. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  71. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  72. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  73. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  74. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  75. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  76. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  77. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  78. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  79. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  80. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  81. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  82. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  83. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  84. package/src/subgraph/graph-ts.ts +2285 -0
  85. package/src/subgraph/hosts.ts +158 -0
  86. package/src/subgraph/rpc.ts +65 -0
  87. package/src/subgraph/runtime.ts +595 -0
  88. package/src/subgraph/scope.ts +68 -0
  89. package/src/tui/InitApi.res +85 -0
  90. package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
  91. package/src/tui/Tui.res +142 -249
  92. package/src/tui/Tui.res.mjs +123 -332
  93. package/src/Js.shim.ts +0 -11
  94. package/src/bindings/DateFns.res +0 -74
  95. package/src/bindings/DateFns.res.mjs +0 -22
  96. package/src/sources/FuelSDK.res +0 -38
  97. package/src/sources/FuelSDK.res.mjs +0 -27
  98. package/src/sources/vendored-fuel-abi-coder.js +0 -1792
  99. package/src/tui/bindings/Ink.res +0 -369
  100. package/src/tui/bindings/Ink.res.mjs +0 -66
  101. package/src/tui/bindings/Style.res +0 -123
  102. package/src/tui/bindings/Style.res.mjs +0 -2
  103. package/src/tui/components/BufferedProgressBar.res +0 -40
  104. package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
  105. package/src/tui/components/CustomHooks.res +0 -122
  106. package/src/tui/components/Messages.res +0 -40
  107. package/src/tui/components/Messages.res.mjs +0 -71
  108. package/src/tui/components/SyncETA.res +0 -179
  109. package/src/tui/components/SyncETA.res.mjs +0 -249
  110. package/src/tui/components/TuiData.res +0 -98
  111. package/src/tui/components/TuiData.res.mjs +0 -73
@@ -0,0 +1,85 @@
1
+ type ecosystem = | @as("evm") Evm | @as("fuel") Fuel | @as("svm") Svm
2
+ type body = {
3
+ envioVersion: string,
4
+ envioApiToken: option<string>,
5
+ ecosystem: ecosystem,
6
+ hyperSyncNetworks: array<ChainId.t>,
7
+ rpcNetworks: array<ChainId.t>,
8
+ }
9
+
10
+ let bodySchema = S.object(s => {
11
+ envioVersion: s.field("envioVersion", S.string),
12
+ envioApiToken: s.field("envioApiToken", S.option(S.string)),
13
+ ecosystem: s.field("ecosystem", S.enum([Evm, Fuel, Svm])),
14
+ hyperSyncNetworks: s.field("hyperSyncNetworks", S.array(ChainId.schema)),
15
+ rpcNetworks: s.field("rpcNetworks", S.array(ChainId.schema)),
16
+ })
17
+
18
+ let makeBody = (~envioVersion, ~envioApiToken, ~config: Config.t) => {
19
+ let hyperSyncNetworks = []
20
+ let rpcNetworks = []
21
+ config.chainMap
22
+ ->ChainMap.values
23
+ ->Array.forEach(({sourceConfig, id}) => {
24
+ let usesHyperSync = switch sourceConfig {
25
+ | Config.EvmSourceConfig({hypersync: Some(_)}) => true
26
+ | Config.EvmSourceConfig({hypersync: None}) => false
27
+ | Config.FuelSourceConfig(_) => true // Fuel always uses HyperFuel
28
+ | Config.SvmSourceConfig(_) => false
29
+ | Config.SimulateSourceConfig(_) => false
30
+ | Config.CustomSources(sources) => sources->Array.some(s => s.poweredByHyperSync)
31
+ }
32
+ switch usesHyperSync {
33
+ | true => hyperSyncNetworks
34
+ | false => rpcNetworks
35
+ }
36
+ ->Array.push(id)
37
+ ->ignore
38
+ })
39
+
40
+ {
41
+ envioVersion,
42
+ envioApiToken,
43
+ ecosystem: (config.ecosystem.name :> ecosystem),
44
+ hyperSyncNetworks,
45
+ rpcNetworks,
46
+ }
47
+ }
48
+
49
+ type messageColor =
50
+ | @as("primary") Primary
51
+ | @as("secondary") Secondary
52
+ | @as("info") Info
53
+ | @as("danger") Danger
54
+ | @as("success") Success
55
+ | @as("white") White
56
+ | @as("gray") Gray
57
+
58
+ type message = {
59
+ color: messageColor,
60
+ content: string,
61
+ }
62
+
63
+ let messageSchema = S.object(s => {
64
+ color: s.field("color", S.enum([Primary, Secondary, Info, Danger, Success, White, Gray])),
65
+ content: s.field("content", S.string),
66
+ })
67
+
68
+ let client = Rest.client(Env.envioAppUrl ++ "/api")
69
+
70
+ let route = Rest.route(() => {
71
+ method: Post,
72
+ path: "/hyperindex/init",
73
+ input: s => s.body(bodySchema),
74
+ responses: [s => s.field("messages", S.array(messageSchema))],
75
+ })
76
+
77
+ let getMessages = async (~config) => {
78
+ let envioVersion = Utils.EnvioPackage.value.version
79
+ let body = makeBody(~envioVersion, ~envioApiToken=Env.envioApiToken, ~config)
80
+
81
+ switch await route->Rest.fetch(body, ~client) {
82
+ | exception exn => Error(exn)
83
+ | messages => Ok(messages)
84
+ }
85
+ }
@@ -1,12 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Env from "../../Env.res.mjs";
4
- import * as Rest from "../../vendored/Rest.res.mjs";
5
- import * as Utils from "../../Utils.res.mjs";
6
- import * as React from "react";
7
- import * as ChainId from "../../ChainId.res.mjs";
8
- import * as Logging from "../../Logging.res.mjs";
9
- import * as ChainMap from "../../ChainMap.res.mjs";
3
+ import * as Env from "../Env.res.mjs";
4
+ import * as Rest from "../vendored/Rest.res.mjs";
5
+ import * as Utils from "../Utils.res.mjs";
6
+ import * as ChainId from "../ChainId.res.mjs";
7
+ import * as ChainMap from "../ChainMap.res.mjs";
10
8
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
11
9
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
10
 
@@ -56,25 +54,6 @@ function makeBody(envioVersion, envioApiToken, config) {
56
54
  };
57
55
  }
58
56
 
59
- function toTheme(color) {
60
- switch (color) {
61
- case "primary" :
62
- return "#9860E5";
63
- case "secondary" :
64
- return "#FFBB2F";
65
- case "info" :
66
- return "#6CBFEE";
67
- case "danger" :
68
- return "#FF8269";
69
- case "success" :
70
- return "#3B8C3D";
71
- case "white" :
72
- return "white";
73
- case "gray" :
74
- return "gray";
75
- }
76
- }
77
-
78
57
  let messageSchema = S$RescriptSchema.object(s => ({
79
58
  color: s.f("color", S$RescriptSchema.$$enum([
80
59
  "primary",
@@ -118,44 +97,12 @@ async function getMessages(config) {
118
97
  };
119
98
  }
120
99
 
121
- let InitApi = {
122
- bodySchema: bodySchema,
123
- makeBody: makeBody,
124
- toTheme: toTheme,
125
- messageSchema: messageSchema,
126
- client: client,
127
- route: route,
128
- getMessages: getMessages
129
- };
130
-
131
- function useMessages(config) {
132
- let match = React.useState(() => "Loading");
133
- let setRequest = match[1];
134
- React.useEffect(() => {
135
- getMessages(config).then(res => {
136
- if (res.TAG === "Ok") {
137
- let data = res._0;
138
- return setRequest(param => ({
139
- TAG: "Data",
140
- _0: data
141
- }));
142
- }
143
- let e = res._0;
144
- Logging.error({
145
- msg: "Failed to load messages from envio server",
146
- err: Utils.prettifyExn(e)
147
- });
148
- setRequest(param => ({
149
- TAG: "Err",
150
- _0: e
151
- }));
152
- });
153
- }, []);
154
- return match[0];
155
- }
156
-
157
100
  export {
158
- InitApi,
159
- useMessages,
101
+ bodySchema,
102
+ makeBody,
103
+ messageSchema,
104
+ client,
105
+ route,
106
+ getMessages,
160
107
  }
161
108
  /* bodySchema Not a pure module */
package/src/tui/Tui.res CHANGED
@@ -1,252 +1,38 @@
1
- open Ink
2
-
3
- module ChainLine = {
4
- @react.component
5
- let make = (
6
- ~chainId,
7
- ~maxChainIdLength,
8
- ~stdoutColumns: int,
9
- ~progressBlock: int,
10
- ~bufferBlock: int,
11
- ~toBlock: int,
12
- ~startBlock,
13
- ~endBlock,
14
- ~poweredByHyperSync,
15
- ~eventsProcessed,
16
- ~blockUnit: string,
17
- ) => {
18
- let chainsWidth = Pervasives.min(stdoutColumns - 2, 60)
19
- let headerWidth = maxChainIdLength + 10 // 10 for additional text
20
-
21
- let progressBlockStr = progressBlock->TuiData.formatLocaleString
22
- let toBlockStr = toBlock->TuiData.formatLocaleString
23
- let eventsStr = eventsProcessed->TuiData.formatFloatLocaleString
24
-
25
- let endLabel = ` (End ${blockUnit})`
26
- let blocksText =
27
- `${blockUnit}s: ${progressBlockStr} / ${toBlockStr}` ++
28
- (endBlock->Option.isSome ? endLabel : "") ++ ` `
29
- let eventsText = `Events: ${eventsStr}`
30
-
31
- let fitsSameLine = blocksText->String.length + eventsText->String.length <= chainsWidth
32
-
33
- <Box flexDirection={Column}>
34
- <Box flexDirection=Row width=Num(chainsWidth)>
35
- <Box width={Num(headerWidth)}>
36
- <Text> {"Chain: "->React.string} </Text>
37
- <Text bold=true> {chainId->React.string} </Text>
38
- <Text> {" "->React.string} </Text>
39
- {poweredByHyperSync ? <Text color=Secondary> {"⚡"->React.string} </Text> : React.null}
40
- </Box>
41
- <BufferedProgressBar
42
- barWidth={chainsWidth - headerWidth}
43
- loaded={progressBlock - startBlock}
44
- buffered={bufferBlock - startBlock}
45
- outOf={toBlock - startBlock}
46
- loadingColor={Secondary}
47
- />
48
- </Box>
49
- <Box flexDirection={Row}>
50
- <Text color={Gray}> {blocksText->React.string} </Text>
51
- {fitsSameLine ? <Text color={Gray}> {eventsText->React.string} </Text> : React.null}
52
- </Box>
53
- {fitsSameLine
54
- ? React.null
55
- : <Box flexDirection={Row}>
56
- <Text color={Gray}> {eventsText->String.trim->React.string} </Text>
57
- </Box>}
58
- <Newline />
59
- </Box>
60
- }
1
+ type t
2
+
3
+ type info = {
4
+ ecosystem: string,
5
+ version: string,
6
+ projectDir: string,
7
+ startTime: float,
8
+ graphqlUrl: string,
9
+ graphqlPassword: option<string>,
10
+ devConsoleUrl: option<string>,
11
+ clickhouseUrl: option<string>,
61
12
  }
62
13
 
63
- module EventsPerSecond = {
64
- type sample = {time: float, events: float}
65
-
66
- let windowMs = 60_000.
67
-
68
- let computeEps = (samples: array<sample>) => {
69
- let len = samples->Array.length
70
- switch (samples->Array.get(0), samples->Array.get(len - 1)) {
71
- | (Some(first), Some(last)) if last.time > first.time =>
72
- Some((last.events -. first.events) /. ((last.time -. first.time) /. 1000.))
73
- | _ => None
74
- }
75
- }
76
-
77
- let use = (~totalEventsProcessed: float, ~tick: int) => {
78
- let (samples, setSamples) = React.useState((): array<sample> => [])
79
-
80
- React.useEffect1(() => {
81
- let now = Date.now()
82
- let cutoff = now -. windowMs
83
- setSamples(prev => {
84
- let kept = prev->Array.filter(s => s.time >= cutoff)
85
- kept->Array.concat([{time: now, events: totalEventsProcessed}])
86
- })
87
- None
88
- }, [tick])
89
-
90
- computeEps(samples)
91
- }
92
- }
93
-
94
- module TotalEventsProcessed = {
95
- @react.component
96
- let make = (~totalEventsProcessed, ~eventsPerSecond: option<float>) => {
97
- <Text>
98
- <Text bold=true> {"Total Events: "->React.string} </Text>
99
- <Text color={Secondary}>
100
- {`${totalEventsProcessed->TuiData.formatFloatLocaleString}`->React.string}
101
- </Text>
102
- {switch eventsPerSecond {
103
- | Some(eps) =>
104
- <Text color={Gray}>
105
- {` (${Math.round(eps)->TuiData.formatFloatLocaleString} events/sec)`->React.string}
106
- </Text>
107
- | None => React.null
108
- }}
109
- </Text>
110
- }
14
+ type chain = {
15
+ chainId: string,
16
+ poweredByHyperSync: bool,
17
+ startBlock: int,
18
+ endBlock: option<int>,
19
+ firstEventBlockNumber: option<int>,
20
+ progressBlockNumber: int,
21
+ latestFetchedBlockNumber: int,
22
+ knownHeight: int,
23
+ sourceBlockNumber: int,
24
+ timestampCaughtUpToHeadOrEndblock: option<float>,
25
+ numEventsProcessed: float,
26
+ rateLimitTimeMs: float,
27
+ rateLimitResetInMs: option<float>,
28
+ processedToEndblock: bool,
111
29
  }
112
30
 
113
- module App = {
114
- @react.component
115
- let make = (~config: Config.t, ~getMetrics) => {
116
- let stdoutColumns = Hooks.useStdoutColumns()
117
- // Metrics are rebuilt from state mutated in place — passing the same value
118
- // to useState would bail out via Object.is and skip the re-render. Tick a
119
- // counter instead and read metrics freshly on every render.
120
- let (tick, setTick) = React.useState(() => 0)
121
- let metrics: Metrics.t = getMetrics()
122
-
123
- React.useEffect(() => {
124
- let intervalId = setInterval(() => {
125
- setTick(t => t + 1)
126
- }, 500)
127
-
128
- Some(
129
- () => {
130
- clearInterval(intervalId)
131
- },
132
- )
133
- }, [getMetrics])
134
-
135
- let blockUnit = switch config.ecosystem.name {
136
- | Svm => "Slot"
137
- | Evm | Fuel => "Block"
138
- }
139
- let chains = metrics.chains->Array.map(m => m->TuiData.fromChainMetrics(~blockUnit))
140
-
141
- let totalEventsProcessed = chains->Array.reduce(0., (acc, chain) => {
142
- acc +. chain.eventsProcessed
143
- })
144
- let maxChainIdLength = chains->Array.reduce(0, (acc, chain) => {
145
- let chainIdLength = chain.chainId->String.length
146
- if chainIdLength > acc {
147
- chainIdLength
148
- } else {
149
- acc
150
- }
151
- })
152
- let eventsPerSecond = EventsPerSecond.use(~totalEventsProcessed, ~tick)
153
-
154
- <Box flexDirection={Column}>
155
- <BigText
156
- text="envio"
157
- colors=[Secondary, Primary]
158
- font={chains->Array.length > 5 ? Tiny : Block}
159
- space=false
160
- />
161
- <Newline />
162
- {chains
163
- ->Array.mapWithIndex((chainData, i) => {
164
- <ChainLine
165
- key={i->Int.toString}
166
- chainId={chainData.chainId}
167
- maxChainIdLength={maxChainIdLength}
168
- progressBlock={chainData.progressBlock}
169
- bufferBlock={chainData.bufferBlock}
170
- toBlock={chainData.toBlock}
171
- startBlock={chainData.startBlock}
172
- endBlock={chainData.endBlock}
173
- stdoutColumns={stdoutColumns}
174
- poweredByHyperSync={chainData.poweredByHyperSync}
175
- eventsProcessed={chainData.eventsProcessed}
176
- blockUnit={chainData.blockUnit}
177
- />
178
- })
179
- ->React.array}
180
- <TotalEventsProcessed
181
- totalEventsProcessed
182
- eventsPerSecond={SyncETA.isIndexerFullySynced(chains) ? None : eventsPerSecond}
183
- />
184
- <SyncETA chains indexerStartTime={metrics.startTime} />
185
- {
186
- let maxRateLimitTimeMs =
187
- chains->Array.reduce(0., (acc, chain) => Pervasives.max(acc, chain.rateLimitTimeMs))
188
- let maxResetInMs =
189
- chains->Array.reduce(0.0, (acc, chain) =>
190
- Pervasives.max(acc, chain.rateLimitResetInMs->Option.getOr(0.0))
191
- )
192
- maxRateLimitTimeMs > 1000.
193
- ? {
194
- let rateLimitSecs = Math.round(maxRateLimitTimeMs /. 1000.)
195
- let activeSuffix = if maxResetInMs > 0.0 {
196
- let resetSecs = Pervasives.max(1.0, Math.ceil(maxResetInMs /. 1000.))
197
- ` (⏳ ${resetSecs->TuiData.formatFloatLocaleString}s until reset)`
198
- } else {
199
- ""
200
- }
201
- <Box flexDirection={Column}>
202
- <Newline />
203
- <Text color={Danger}>
204
- {`Backfill ${rateLimitSecs->TuiData.formatFloatLocaleString}s slower due to your plan's rate limit${activeSuffix}`->React.string}
205
- </Text>
206
- <Text color={Danger}>
207
- <Text color={Danger}> {"Upgrade at "->React.string} </Text>
208
- <Text color={Danger} underline=true>
209
- {"https://envio.dev/app/api-tokens"->React.string}
210
- </Text>
211
- <Text color={Danger}> {" for higher rate limits."->React.string} </Text>
212
- </Text>
213
- </Box>
214
- }
215
- : React.null
216
- }
217
- <Newline />
218
- <Box flexDirection={Row}>
219
- <Text> {"GraphQL: "->React.string} </Text>
220
- <Text color={Info} underline=true> {Env.Hasura.url->React.string} </Text>
221
- {
222
- let defaultPassword = "testing"
223
- if Env.Hasura.secret == defaultPassword {
224
- <Text color={Gray}> {` (password: ${defaultPassword})`->React.string} </Text>
225
- } else {
226
- React.null
227
- }
228
- }
229
- </Box>
230
- {if config.isDev {
231
- <Box flexDirection={Row}>
232
- <Text> {"Dev Console: "->React.string} </Text>
233
- <Text color={Info} underline=true> {`${Env.envioAppUrl}/console`->React.string} </Text>
234
- </Box>
235
- } else {
236
- React.null
237
- }}
238
- {switch (config.storage.clickhouse, Env.ClickHouse.host()) {
239
- | (true, Some(host)) =>
240
- <Box flexDirection={Row}>
241
- <Text> {"ClickHouse: "->React.string} </Text>
242
- <Text color={Info} underline=true> {`${host}/play`->React.string} </Text>
243
- </Box>
244
- | _ => React.null
245
- }}
246
- <Messages config />
247
- </Box>
248
- }
249
- }
31
+ @send external make: (Core.tuiCtor, info) => t = "start"
32
+ @send external update: (t, array<chain>) => unit = "update"
33
+ @send external setMessages: (t, Null.t<array<InitApi.message>>) => unit = "setMessages"
34
+ @send external print: (t, string) => bool = "print"
35
+ @send external stop: t => unit = "stop"
250
36
 
251
37
  // Whether this process draws the progress display: `ENVIO_TUI` first, then
252
38
  // whether anything is watching. A supervisor asks the same question its
@@ -258,9 +44,116 @@ let shouldUse = (~suppressed=false, ~explicitTui=Env.tuiEnvVar) =>
258
44
  | (_, None) => !Envio.isNonInteractive()
259
45
  }
260
46
 
261
- let start = (~config, ~getMetrics) => {
262
- let {rerender} = render(<App config getMetrics />)
263
- () => {
264
- rerender(<App config getMetrics />)
47
+ let toChain = (m: Metrics.chainMetrics): chain => {
48
+ chainId: m.chainId->ChainId.toString,
49
+ poweredByHyperSync: m.poweredByHyperSync,
50
+ startBlock: m.startBlock,
51
+ endBlock: m.endBlock,
52
+ firstEventBlockNumber: m.firstEventBlockNumber,
53
+ progressBlockNumber: m.progressBlockNumber,
54
+ latestFetchedBlockNumber: m.latestFetchedBlockNumber,
55
+ knownHeight: m.knownHeight,
56
+ sourceBlockNumber: m.sourceBlockNumber,
57
+ timestampCaughtUpToHeadOrEndblock: m.timestampCaughtUpToHeadOrEndblock->Option.map(Date.getTime),
58
+ numEventsProcessed: m.numEventsProcessed,
59
+ rateLimitTimeMs: m.rateLimitTimeMs,
60
+ rateLimitResetInMs: m.rateLimitResetInMs,
61
+ processedToEndblock: m->Metrics.hasProcessedToEndblock,
62
+ }
63
+
64
+ type consoleClass
65
+ type writableClass
66
+ @module("node:console") external consoleClass: consoleClass = "Console"
67
+ @module("node:stream") external writableClass: writableClass = "Writable"
68
+
69
+ // Sends everything written through `console` above the display, the way the
70
+ // terminal would have shown it, and returns the function that restores it.
71
+ // Routed through a `Console` of our own so every method formats as Node's
72
+ // would; stderr stays where it is when it isn't the terminal, and once the
73
+ // display is gone, output goes back to where it came from.
74
+ let redirectConsole: (
75
+ consoleClass,
76
+ writableClass,
77
+ string => bool,
78
+ ) => unit => unit = %raw(`(Console, Writable, print) => {
79
+ let original = [];
80
+ const restore = () => original.forEach(([name, method]) => { console[name] = method; });
81
+ const toDisplay = (stream) => new Writable({
82
+ write(chunk, _encoding, callback) {
83
+ // Each call ends its output with a newline, which printing a line adds.
84
+ if (!print(chunk.toString().replace(/\n$/, ""))) {
85
+ restore();
86
+ stream.write(chunk);
87
+ }
88
+ callback();
89
+ },
90
+ });
91
+ const redirected = new Console({
92
+ stdout: toDisplay(process.stdout),
93
+ stderr: process.stderr.isTTY ? toDisplay(process.stderr) : process.stderr,
94
+ colorMode: process.stdout.hasColors(),
95
+ });
96
+ const methods = Object.keys(console).filter((name) => typeof redirected[name] === "function");
97
+ original = methods.map((name) => [name, console[name]]);
98
+ methods.forEach((name) => { console[name] = redirected[name].bind(redirected); });
99
+ return restore;
100
+ }`)
101
+
102
+ let start = (~config: Config.t, ~getMetrics: unit => Metrics.t) => {
103
+ let metrics = getMetrics()
104
+ let info = {
105
+ ecosystem: (config.ecosystem.name :> string),
106
+ version: Utils.EnvioPackage.value.version,
107
+ projectDir: NodeJs.Process.cwd(),
108
+ startTime: metrics.startTime->Date.getTime,
109
+ graphqlUrl: Env.Hasura.url,
110
+ graphqlPassword: Env.Hasura.secret === "testing" ? Some("testing") : None,
111
+ devConsoleUrl: config.isDev ? Some(`${Env.envioAppUrl}/console`) : None,
112
+ clickhouseUrl: switch (config.storage.clickhouse, Env.ClickHouse.host()) {
113
+ | (true, Some(host)) => Some(`${host}/play`)
114
+ | _ => None
115
+ },
116
+ }
117
+ switch Core.getAddon().tui->make(info) {
118
+ | exception exn =>
119
+ Logging.warn({
120
+ "msg": "Failed to start the TUI. Continuing without it.",
121
+ "err": exn->Utils.prettifyExn,
122
+ })
123
+ | tui =>
124
+ let restoreConsole = redirectConsole(consoleClass, writableClass, text => tui->print(text))
125
+ let update = () => tui->update(getMetrics().chains->Array.map(toChain))
126
+ update()
127
+ let _ = setInterval(update, 500)
128
+ let finish = () => {
129
+ tui->stop
130
+ restoreConsole()
131
+ }
132
+ NodeJs.Process.onExit(finish)
133
+ // A signal nothing else listens for ends the process without an `exit`
134
+ // event, so the display finishes first and the signal is raised again
135
+ // once its default action is back.
136
+ ["SIGINT", "SIGTERM", "SIGHUP"]->Array.forEach(signal => {
137
+ let rec onSignal = () =>
138
+ if NodeJs.Process.listenerCount(signal) === 1 {
139
+ finish()
140
+ NodeJs.Process.offSignal(signal, onSignal)
141
+ NodeJs.Process.kill(NodeJs.Process.pid, signal)->ignore
142
+ }
143
+ NodeJs.Process.onSignal(signal, onSignal)
144
+ })
145
+ InitApi.getMessages(~config)
146
+ ->Promise.thenResolve(result =>
147
+ switch result {
148
+ | Ok(messages) => tui->setMessages(Null.Value(messages))
149
+ | Error(exn) =>
150
+ Logging.error({
151
+ "msg": "Failed to load messages from envio server",
152
+ "err": exn->Utils.prettifyExn,
153
+ })
154
+ tui->setMessages(Null.Null)
155
+ }
156
+ )
157
+ ->ignore
265
158
  }
266
159
  }