envio 3.13.0 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) 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 +69 -100
  6. package/src/Config.res.mjs +50 -63
  7. package/src/Core.res +46 -6
  8. package/src/Core.res.mjs +41 -4
  9. package/src/EventConfigBuilder.res +4 -10
  10. package/src/EventConfigBuilder.res.mjs +1 -3
  11. package/src/ExitOnCaughtUp.res +5 -1
  12. package/src/ExitOnCaughtUp.res.mjs +8 -3
  13. package/src/HandlerLoader.res +74 -32
  14. package/src/HandlerLoader.res.mjs +69 -23
  15. package/src/HandlerTypeCheck.mjs +14 -0
  16. package/src/HandlerTypeCheckWorker.mjs +68 -0
  17. package/src/Internal.res +1 -1
  18. package/src/Main.res +17 -12
  19. package/src/Main.res.mjs +9 -3
  20. package/src/NodeVersion.mjs +13 -0
  21. package/src/Persistence.res +79 -38
  22. package/src/Persistence.res.mjs +41 -10
  23. package/src/PgStorage.res +62 -34
  24. package/src/PgStorage.res.mjs +65 -31
  25. package/src/ResumePlan.res +172 -0
  26. package/src/ResumePlan.res.mjs +122 -0
  27. package/src/Supervisor.res +8 -8
  28. package/src/Supervisor.res.mjs +6 -1
  29. package/src/TestIndexer.res +14 -4
  30. package/src/TestIndexer.res.mjs +3 -2
  31. package/src/TsModuleHooks.mjs +121 -0
  32. package/src/Utils.res +0 -8
  33. package/src/Worker.res +8 -0
  34. package/src/Worker.res.mjs +11 -0
  35. package/src/bindings/NodeJs.res +8 -7
  36. package/src/bindings/NodeJs.res.mjs +0 -3
  37. package/src/bindings/Pino.res +2 -2
  38. package/src/db/InternalTable.res +52 -2
  39. package/src/db/InternalTable.res.mjs +32 -3
  40. package/src/sources/FuelHyperSync.res +2 -0
  41. package/src/sources/FuelHyperSync.res.mjs +1 -0
  42. package/src/sources/FuelHyperSync.resi +1 -0
  43. package/src/sources/FuelHyperSyncClient.res +25 -8
  44. package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
  45. package/src/sources/FuelHyperSyncSource.res +24 -22
  46. package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
  47. package/src/tui/InitApi.res +85 -0
  48. package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
  49. package/src/tui/Tui.res +142 -249
  50. package/src/tui/Tui.res.mjs +123 -332
  51. package/src/Js.shim.ts +0 -11
  52. package/src/bindings/DateFns.res +0 -74
  53. package/src/bindings/DateFns.res.mjs +0 -22
  54. package/src/sources/FuelSDK.res +0 -38
  55. package/src/sources/FuelSDK.res.mjs +0 -27
  56. package/src/sources/vendored-fuel-abi-coder.js +0 -1792
  57. package/src/tui/bindings/Ink.res +0 -369
  58. package/src/tui/bindings/Ink.res.mjs +0 -66
  59. package/src/tui/bindings/Style.res +0 -123
  60. package/src/tui/bindings/Style.res.mjs +0 -2
  61. package/src/tui/components/BufferedProgressBar.res +0 -40
  62. package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
  63. package/src/tui/components/CustomHooks.res +0 -122
  64. package/src/tui/components/Messages.res +0 -40
  65. package/src/tui/components/Messages.res.mjs +0 -71
  66. package/src/tui/components/SyncETA.res +0 -179
  67. package/src/tui/components/SyncETA.res.mjs +0 -249
  68. package/src/tui/components/TuiData.res +0 -98
  69. package/src/tui/components/TuiData.res.mjs +0 -73
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
  }