envio 3.0.0-alpha.2 → 3.0.0-alpha.20

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 (175) hide show
  1. package/README.md +164 -30
  2. package/bin.mjs +49 -0
  3. package/evm.schema.json +79 -169
  4. package/fuel.schema.json +50 -21
  5. package/index.d.ts +497 -1
  6. package/index.js +4 -0
  7. package/package.json +42 -31
  8. package/rescript.json +4 -1
  9. package/src/Batch.res +11 -8
  10. package/src/Batch.res.mjs +11 -9
  11. package/src/ChainFetcher.res +531 -0
  12. package/src/ChainFetcher.res.mjs +339 -0
  13. package/src/ChainManager.res +190 -0
  14. package/src/ChainManager.res.mjs +166 -0
  15. package/src/Change.res +3 -3
  16. package/src/Config.gen.ts +19 -0
  17. package/src/Config.res +737 -22
  18. package/src/Config.res.mjs +703 -26
  19. package/src/{Indexer.res → Ctx.res} +1 -1
  20. package/src/Ecosystem.res +9 -124
  21. package/src/Ecosystem.res.mjs +19 -160
  22. package/src/Env.res +30 -74
  23. package/src/Env.res.mjs +25 -87
  24. package/src/Envio.gen.ts +3 -1
  25. package/src/Envio.res +20 -9
  26. package/src/EventProcessing.res +469 -0
  27. package/src/EventProcessing.res.mjs +337 -0
  28. package/src/EvmTypes.gen.ts +6 -0
  29. package/src/EvmTypes.res +1 -0
  30. package/src/FetchState.res +1256 -639
  31. package/src/FetchState.res.mjs +1135 -612
  32. package/src/GlobalState.res +1190 -0
  33. package/src/GlobalState.res.mjs +1183 -0
  34. package/src/GlobalStateManager.res +68 -0
  35. package/src/GlobalStateManager.res.mjs +75 -0
  36. package/src/GlobalStateManager.resi +7 -0
  37. package/src/HandlerLoader.res +89 -0
  38. package/src/HandlerLoader.res.mjs +79 -0
  39. package/src/HandlerRegister.res +357 -0
  40. package/src/HandlerRegister.res.mjs +299 -0
  41. package/src/{EventRegister.resi → HandlerRegister.resi} +13 -13
  42. package/src/Hasura.res +111 -175
  43. package/src/Hasura.res.mjs +88 -150
  44. package/src/InMemoryStore.res +1 -1
  45. package/src/InMemoryStore.res.mjs +3 -3
  46. package/src/InMemoryTable.res +1 -1
  47. package/src/InMemoryTable.res.mjs +1 -1
  48. package/src/Internal.gen.ts +4 -0
  49. package/src/Internal.res +230 -12
  50. package/src/Internal.res.mjs +115 -1
  51. package/src/LoadLayer.res +444 -0
  52. package/src/LoadLayer.res.mjs +296 -0
  53. package/src/LoadLayer.resi +32 -0
  54. package/src/LogSelection.res +33 -27
  55. package/src/LogSelection.res.mjs +6 -0
  56. package/src/Logging.res +21 -7
  57. package/src/Logging.res.mjs +16 -8
  58. package/src/Main.res +377 -0
  59. package/src/Main.res.mjs +339 -0
  60. package/src/Persistence.res +7 -21
  61. package/src/Persistence.res.mjs +3 -3
  62. package/src/PgStorage.gen.ts +10 -0
  63. package/src/PgStorage.res +116 -69
  64. package/src/PgStorage.res.d.mts +5 -0
  65. package/src/PgStorage.res.mjs +93 -50
  66. package/src/Prometheus.res +294 -224
  67. package/src/Prometheus.res.mjs +353 -340
  68. package/src/ReorgDetection.res +6 -10
  69. package/src/ReorgDetection.res.mjs +6 -6
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +2 -2
  72. package/src/Sink.res +4 -2
  73. package/src/Sink.res.mjs +2 -1
  74. package/src/TableIndices.res +0 -1
  75. package/src/TestIndexer.res +692 -0
  76. package/src/TestIndexer.res.mjs +527 -0
  77. package/src/TestIndexerProxyStorage.res +205 -0
  78. package/src/TestIndexerProxyStorage.res.mjs +151 -0
  79. package/src/TopicFilter.res +1 -1
  80. package/src/Types.ts +1 -1
  81. package/src/UserContext.res +424 -0
  82. package/src/UserContext.res.mjs +279 -0
  83. package/src/Utils.res +97 -26
  84. package/src/Utils.res.mjs +91 -44
  85. package/src/bindings/BigInt.res +10 -0
  86. package/src/bindings/BigInt.res.mjs +15 -0
  87. package/src/bindings/ClickHouse.res +120 -23
  88. package/src/bindings/ClickHouse.res.mjs +118 -28
  89. package/src/bindings/DateFns.res +74 -0
  90. package/src/bindings/DateFns.res.mjs +22 -0
  91. package/src/bindings/EventSource.res +8 -1
  92. package/src/bindings/EventSource.res.mjs +8 -1
  93. package/src/bindings/Express.res +1 -0
  94. package/src/bindings/Hrtime.res +14 -1
  95. package/src/bindings/Hrtime.res.mjs +22 -2
  96. package/src/bindings/Hrtime.resi +4 -0
  97. package/src/bindings/Lodash.res +0 -1
  98. package/src/bindings/NodeJs.res +49 -3
  99. package/src/bindings/NodeJs.res.mjs +11 -3
  100. package/src/bindings/Pino.res +24 -10
  101. package/src/bindings/Pino.res.mjs +14 -8
  102. package/src/bindings/Postgres.gen.ts +8 -0
  103. package/src/bindings/Postgres.res +5 -1
  104. package/src/bindings/Postgres.res.d.mts +5 -0
  105. package/src/bindings/PromClient.res +0 -10
  106. package/src/bindings/PromClient.res.mjs +0 -3
  107. package/src/bindings/Vitest.res +142 -0
  108. package/src/bindings/Vitest.res.mjs +9 -0
  109. package/src/bindings/WebSocket.res +27 -0
  110. package/src/bindings/WebSocket.res.mjs +2 -0
  111. package/src/bindings/Yargs.res +8 -0
  112. package/src/bindings/Yargs.res.mjs +2 -0
  113. package/src/db/EntityHistory.res +7 -7
  114. package/src/db/EntityHistory.res.mjs +9 -9
  115. package/src/db/InternalTable.res +59 -111
  116. package/src/db/InternalTable.res.mjs +73 -104
  117. package/src/db/Table.res +27 -8
  118. package/src/db/Table.res.mjs +25 -14
  119. package/src/sources/Evm.res +84 -0
  120. package/src/sources/Evm.res.mjs +105 -0
  121. package/src/sources/EvmChain.res +94 -0
  122. package/src/sources/EvmChain.res.mjs +60 -0
  123. package/src/sources/Fuel.res +19 -34
  124. package/src/sources/Fuel.res.mjs +34 -16
  125. package/src/sources/FuelSDK.res +38 -0
  126. package/src/sources/FuelSDK.res.mjs +29 -0
  127. package/src/sources/HyperFuel.res +2 -2
  128. package/src/sources/HyperFuel.resi +1 -1
  129. package/src/sources/HyperFuelClient.res +2 -2
  130. package/src/sources/HyperFuelSource.res +33 -13
  131. package/src/sources/HyperFuelSource.res.mjs +24 -16
  132. package/src/sources/HyperSync.res +36 -6
  133. package/src/sources/HyperSync.res.mjs +9 -7
  134. package/src/sources/HyperSync.resi +4 -0
  135. package/src/sources/HyperSyncClient.res +1 -1
  136. package/src/sources/HyperSyncHeightStream.res +47 -116
  137. package/src/sources/HyperSyncHeightStream.res.mjs +46 -73
  138. package/src/sources/HyperSyncSource.res +118 -139
  139. package/src/sources/HyperSyncSource.res.mjs +104 -121
  140. package/src/sources/Rpc.res +86 -14
  141. package/src/sources/Rpc.res.mjs +101 -9
  142. package/src/sources/RpcSource.res +621 -364
  143. package/src/sources/RpcSource.res.mjs +843 -410
  144. package/src/sources/RpcWebSocketHeightStream.res +181 -0
  145. package/src/sources/RpcWebSocketHeightStream.res.mjs +196 -0
  146. package/src/sources/Source.res +7 -5
  147. package/src/sources/SourceManager.res +325 -225
  148. package/src/sources/SourceManager.res.mjs +314 -171
  149. package/src/sources/SourceManager.resi +17 -6
  150. package/src/sources/Svm.res +81 -0
  151. package/src/sources/Svm.res.mjs +90 -0
  152. package/src/tui/Tui.res +247 -0
  153. package/src/tui/Tui.res.mjs +337 -0
  154. package/src/tui/bindings/Ink.res +371 -0
  155. package/src/tui/bindings/Ink.res.mjs +72 -0
  156. package/src/tui/bindings/Style.res +123 -0
  157. package/src/tui/bindings/Style.res.mjs +2 -0
  158. package/src/tui/components/BufferedProgressBar.res +40 -0
  159. package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
  160. package/src/tui/components/CustomHooks.res +122 -0
  161. package/src/tui/components/CustomHooks.res.mjs +179 -0
  162. package/src/tui/components/Messages.res +41 -0
  163. package/src/tui/components/Messages.res.mjs +75 -0
  164. package/src/tui/components/SyncETA.res +174 -0
  165. package/src/tui/components/SyncETA.res.mjs +263 -0
  166. package/src/tui/components/TuiData.res +47 -0
  167. package/src/tui/components/TuiData.res.mjs +34 -0
  168. package/svm.schema.json +112 -0
  169. package/bin.js +0 -48
  170. package/src/EventRegister.res +0 -241
  171. package/src/EventRegister.res.mjs +0 -240
  172. package/src/bindings/Ethers.gen.ts +0 -14
  173. package/src/bindings/Ethers.res +0 -204
  174. package/src/bindings/Ethers.res.mjs +0 -130
  175. /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
@@ -46,7 +46,10 @@ let getClickHouseFieldType = (
46
46
  let baseType = switch fieldType {
47
47
  | Int32 => "Int32"
48
48
  | Uint32 => "UInt32"
49
+ | UInt52 => "UInt64"
50
+ | UInt64 => "UInt64"
49
51
  | Serial => "Int32"
52
+ | BigSerial => "Int64"
50
53
  | BigInt({?precision}) =>
51
54
  switch precision {
52
55
  | None => "String" // Fallback for unbounded BigInt
@@ -98,6 +101,97 @@ let getClickHouseFieldType = (
98
101
  isNullable ? `Nullable(${baseType})` : baseType
99
102
  }
100
103
 
104
+ // Creates an entity schema from table definition, using clickHouseDate for Date fields
105
+ let makeClickHouseEntitySchema = (table: Table.table): S.t<Internal.entity> => {
106
+ S.schema(s => {
107
+ let dict = Js.Dict.empty()
108
+ table.fields->Belt.Array.forEach(field => {
109
+ switch field {
110
+ | Field(f) => {
111
+ let fieldName = f->Table.getDbFieldName
112
+ let fieldSchema = switch f.fieldType {
113
+ | Date => {
114
+ let dateSchema = Utils.Schema.clickHouseDate->S.toUnknown
115
+ if f.isNullable {
116
+ S.null(dateSchema)->S.toUnknown
117
+ } else if f.isArray {
118
+ S.array(dateSchema)->S.toUnknown
119
+ } else {
120
+ dateSchema
121
+ }
122
+ }
123
+ // ClickHouse returns UInt64 values as strings, need to parse to float
124
+ | UInt52 => {
125
+ let uint52Schema =
126
+ S.float
127
+ ->S.preprocess(_ => {
128
+ parser: unknown =>
129
+ unknown->(Utils.magic: unknown => string)->Js.Float.fromString,
130
+ })
131
+ ->S.toUnknown
132
+ if f.isNullable {
133
+ S.null(uint52Schema)->S.toUnknown
134
+ } else if f.isArray {
135
+ S.array(uint52Schema)->S.toUnknown
136
+ } else {
137
+ uint52Schema
138
+ }
139
+ }
140
+ | _ => f.fieldSchema
141
+ }
142
+ dict->Js.Dict.set(fieldName, s.matches(fieldSchema))
143
+ }
144
+ | DerivedFrom(_) => () // Skip derived fields
145
+ }
146
+ })
147
+ dict->(Utils.magic: Js.Dict.t<unknown> => Internal.entity)
148
+ })
149
+ }
150
+
151
+ let logger = Logging.createChild(~params={"context": "ClickHouse"})
152
+
153
+ // On transient failure, split values in half and retry each half.
154
+ // If only 1 row remains, retry with delay.
155
+ // Delay scales from 100ms to 1000ms as retries decrease.
156
+ let rec insertWithRetry = async (
157
+ client,
158
+ ~table: string,
159
+ ~values: array<'a>,
160
+ ~format: string,
161
+ ~retries=8,
162
+ ) => {
163
+ try {
164
+ await client->insert({table, values, format})
165
+ } catch {
166
+ | exn if retries > 0 =>
167
+ let delayMs = Js.Math.min_int(1000, 100 + 900 * (8 - retries) / 7)
168
+ if Array.length(values) > 1 {
169
+ logger->Logging.childWarn({
170
+ "msg": "ClickHouse insert failed, splitting batch in half and retrying",
171
+ "table": table,
172
+ "batchSize": Array.length(values),
173
+ "retriesLeft": retries,
174
+ "err": exn->Utils.prettifyExn,
175
+ })
176
+ await Utils.delay(delayMs)
177
+ let mid = Array.length(values) / 2
178
+ let first = values->Js.Array2.slice(~start=0, ~end_=mid)
179
+ let second = values->Js.Array2.sliceFrom(mid)
180
+ await insertWithRetry(client, ~table, ~values=first, ~format, ~retries=retries - 1)
181
+ await insertWithRetry(client, ~table, ~values=second, ~format, ~retries=retries - 1)
182
+ } else {
183
+ logger->Logging.childWarn({
184
+ "msg": "ClickHouse insert failed, retrying after delay",
185
+ "table": table,
186
+ "retriesLeft": retries,
187
+ "err": exn->Utils.prettifyExn,
188
+ })
189
+ await Utils.delay(delayMs)
190
+ await insertWithRetry(client, ~table, ~values, ~format, ~retries=retries - 1)
191
+ }
192
+ }
193
+ }
194
+
101
195
  let setCheckpointsOrThrow = async (client, ~batch: Batch.t, ~database: string) => {
102
196
  let checkpointsCount = batch.checkpointIds->Array.length
103
197
  if checkpointsCount === 0 {
@@ -108,7 +202,7 @@ let setCheckpointsOrThrow = async (client, ~batch: Batch.t, ~database: string) =
108
202
  for idx in 0 to checkpointsCount - 1 {
109
203
  checkpointRows
110
204
  ->Js.Array2.push((
111
- batch.checkpointIds->Belt.Array.getUnsafe(idx),
205
+ batch.checkpointIds->Belt.Array.getUnsafe(idx)->BigInt.toString,
112
206
  batch.checkpointChainIds->Belt.Array.getUnsafe(idx),
113
207
  batch.checkpointBlockNumbers->Belt.Array.getUnsafe(idx),
114
208
  batch.checkpointBlockHashes->Belt.Array.getUnsafe(idx),
@@ -118,11 +212,12 @@ let setCheckpointsOrThrow = async (client, ~batch: Batch.t, ~database: string) =
118
212
  }
119
213
 
120
214
  try {
121
- await client->insert({
122
- table: `${database}.\`${InternalTable.Checkpoints.table.tableName}\``,
123
- values: checkpointRows,
124
- format: "JSONCompactEachRow",
125
- })
215
+ await insertWithRetry(
216
+ client,
217
+ ~table=`${database}.\`${InternalTable.Checkpoints.table.tableName}\``,
218
+ ~values=checkpointRows,
219
+ ~format="JSONCompactEachRow",
220
+ )
126
221
  } catch {
127
222
  | exn =>
128
223
  raise(
@@ -135,8 +230,14 @@ let setCheckpointsOrThrow = async (client, ~batch: Batch.t, ~database: string) =
135
230
  }
136
231
  }
137
232
 
233
+ type setUpdatesCache = {
234
+ tableName: string,
235
+ convertOrThrow: Change.t<Internal.entity> => Js.Json.t,
236
+ }
237
+
138
238
  let setUpdatesOrThrow = async (
139
239
  client,
240
+ ~cache: Utils.WeakMap.t<Internal.entityConfig, setUpdatesCache>,
140
241
  ~updates: array<Internal.inMemoryStoreEntityUpdate<Internal.entity>>,
141
242
  ~entityConfig: Internal.entityConfig,
142
243
  ~database: string,
@@ -144,17 +245,17 @@ let setUpdatesOrThrow = async (
144
245
  if updates->Array.length === 0 {
145
246
  ()
146
247
  } else {
147
- let {convertOrThrow, tableName} = switch entityConfig.clickHouseSetUpdatesCache {
148
- | Some(cache) => cache
248
+ let {convertOrThrow, tableName} = switch cache->Utils.WeakMap.get(entityConfig) {
249
+ | Some(cached) => cached
149
250
  | None =>
150
- let cache: Internal.clickHouseSetUpdatesCache = {
251
+ let cached: setUpdatesCache = {
151
252
  tableName: `${database}.\`${EntityHistory.historyTableName(
152
253
  ~entityName=entityConfig.name,
153
254
  ~entityIndex=entityConfig.index,
154
255
  )}\``,
155
256
  convertOrThrow: S.compile(
156
257
  S.union([
157
- EntityHistory.makeSetUpdateSchema(entityConfig.schema),
258
+ EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
158
259
  S.object(s => {
159
260
  s.tag(EntityHistory.changeFieldName, EntityHistory.RowAction.DELETE)
160
261
  Change.Delete({
@@ -173,8 +274,8 @@ let setUpdatesOrThrow = async (
173
274
  ),
174
275
  }
175
276
 
176
- entityConfig.clickHouseSetUpdatesCache = Some(cache)
177
- cache
277
+ cache->Utils.WeakMap.set(entityConfig, cached)->ignore
278
+ cached
178
279
  }
179
280
 
180
281
  try {
@@ -183,11 +284,7 @@ let setUpdatesOrThrow = async (
183
284
  update.latestChange->convertOrThrow
184
285
  })
185
286
 
186
- await client->insert({
187
- table: tableName,
188
- values,
189
- format: "JSONEachRow",
190
- })
287
+ await insertWithRetry(client, ~table=tableName, ~values, ~format="JSONEachRow")
191
288
  } catch {
192
289
  | exn =>
193
290
  raise(
@@ -224,7 +321,7 @@ let makeCreateHistoryTableQuery = (~entityConfig: Internal.entityConfig, ~databa
224
321
  )}\` (
225
322
  ${fieldDefinitions->Js.Array2.joinWith(",\n ")},
226
323
  \`${EntityHistory.checkpointIdFieldName}\` ${getClickHouseFieldType(
227
- ~fieldType=Uint32,
324
+ ~fieldType=UInt64,
228
325
  ~isNullable=false,
229
326
  ~isArray=false,
230
327
  )},
@@ -247,7 +344,7 @@ let makeCreateCheckpointsTableQuery = (~database: string) => {
247
344
  let eventsProcessedField = (#events_processed: InternalTable.Checkpoints.field :> string)
248
345
 
249
346
  `CREATE TABLE IF NOT EXISTS ${database}.\`${InternalTable.Checkpoints.table.tableName}\` (
250
- \`${idField}\` ${getClickHouseFieldType(~fieldType=Int32, ~isNullable=false, ~isArray=false)},
347
+ \`${idField}\` ${getClickHouseFieldType(~fieldType=UInt64, ~isNullable=false, ~isArray=false)},
251
348
  \`${chainIdField}\` ${getClickHouseFieldType(
252
349
  ~fieldType=Int32,
253
350
  ~isNullable=false,
@@ -264,7 +361,7 @@ let makeCreateCheckpointsTableQuery = (~database: string) => {
264
361
  ~isArray=false,
265
362
  )},
266
363
  \`${eventsProcessedField}\` ${getClickHouseFieldType(
267
- ~fieldType=Int32,
364
+ ~fieldType=UInt64,
268
365
  ~isNullable=false,
269
366
  ~isArray=false,
270
367
  )}
@@ -343,7 +440,7 @@ let initialize = async (
343
440
  }
344
441
 
345
442
  // Resume ClickHouse sink after reorg by deleting rows with checkpoint IDs higher than target
346
- let resume = async (client, ~database: string, ~checkpointId: float) => {
443
+ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpointId) => {
347
444
  try {
348
445
  // Try to use the database - will throw if it doesn't exist
349
446
  try {
@@ -368,14 +465,14 @@ let resume = async (client, ~database: string, ~checkpointId: float) => {
368
465
  tables->Belt.Array.map(table => {
369
466
  let tableName = table["name"]
370
467
  client->exec({
371
- query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->Belt.Float.toString}`,
468
+ query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->BigInt.toString}`,
372
469
  })
373
470
  }),
374
471
  )->Promise.ignoreValue
375
472
 
376
473
  // Delete stale checkpoints
377
474
  await client->exec({
378
- query: `DELETE FROM ${database}.\`${InternalTable.Checkpoints.table.tableName}\` WHERE \`${Table.idFieldName}\` > ${checkpointId->Belt.Float.toString}`,
475
+ query: `DELETE FROM ${database}.\`${InternalTable.Checkpoints.table.tableName}\` WHERE \`${Table.idFieldName}\` > ${checkpointId->BigInt.toString}`,
379
476
  })
380
477
  } catch {
381
478
  | Persistence.StorageError(_) as exn => raise(exn)
@@ -21,6 +21,10 @@ function getClickHouseFieldType(fieldType, isNullable, isArray) {
21
21
  case "Uint32" :
22
22
  baseType = "UInt32";
23
23
  break;
24
+ case "UInt52" :
25
+ case "UInt64" :
26
+ baseType = "UInt64";
27
+ break;
24
28
  case "Number" :
25
29
  baseType = "Float64";
26
30
  break;
@@ -28,6 +32,9 @@ function getClickHouseFieldType(fieldType, isNullable, isArray) {
28
32
  case "Serial" :
29
33
  baseType = "Int32";
30
34
  break;
35
+ case "BigSerial" :
36
+ baseType = "Int64";
37
+ break;
31
38
  case "String" :
32
39
  case "Json" :
33
40
  baseType = "String";
@@ -76,6 +83,94 @@ function getClickHouseFieldType(fieldType, isNullable, isArray) {
76
83
  }
77
84
  }
78
85
 
86
+ function makeClickHouseEntitySchema(table) {
87
+ return S$RescriptSchema.schema(function (s) {
88
+ var dict = {};
89
+ Belt_Array.forEach(table.fields, (function (field) {
90
+ if (field.TAG !== "Field") {
91
+ return ;
92
+ }
93
+ var f = field._0;
94
+ var fieldName = Table.getDbFieldName(f);
95
+ var match = f.fieldType;
96
+ var fieldSchema;
97
+ if (typeof match !== "object") {
98
+ switch (match) {
99
+ case "UInt52" :
100
+ var uint52Schema = S$RescriptSchema.preprocess(S$RescriptSchema.$$float, (function (param) {
101
+ return {
102
+ p: (function (unknown) {
103
+ return Number(unknown);
104
+ })
105
+ };
106
+ }));
107
+ fieldSchema = f.isNullable ? S$RescriptSchema.$$null(uint52Schema) : (
108
+ f.isArray ? S$RescriptSchema.array(uint52Schema) : uint52Schema
109
+ );
110
+ break;
111
+ case "Date" :
112
+ var dateSchema = Utils.Schema.clickHouseDate;
113
+ fieldSchema = f.isNullable ? S$RescriptSchema.$$null(dateSchema) : (
114
+ f.isArray ? S$RescriptSchema.array(dateSchema) : dateSchema
115
+ );
116
+ break;
117
+ default:
118
+ fieldSchema = f.fieldSchema;
119
+ }
120
+ } else {
121
+ fieldSchema = f.fieldSchema;
122
+ }
123
+ dict[fieldName] = s.m(fieldSchema);
124
+ }));
125
+ return dict;
126
+ });
127
+ }
128
+
129
+ var logger = Logging.createChild({
130
+ context: "ClickHouse"
131
+ });
132
+
133
+ async function insertWithRetry(client, table, values, format, retriesOpt) {
134
+ var retries = retriesOpt !== undefined ? retriesOpt : 8;
135
+ try {
136
+ return await client.insert({
137
+ table: table,
138
+ values: values,
139
+ format: format
140
+ });
141
+ }
142
+ catch (raw_exn){
143
+ var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
144
+ if (retries > 0) {
145
+ var delayMs = Math.min(1000, 100 + (Math.imul(900, 8 - retries | 0) / 7 | 0) | 0);
146
+ if (values.length > 1) {
147
+ Logging.childWarn(logger, {
148
+ msg: "ClickHouse insert failed, splitting batch in half and retrying",
149
+ table: table,
150
+ batchSize: values.length,
151
+ retriesLeft: retries,
152
+ err: Utils.prettifyExn(exn)
153
+ });
154
+ await Utils.delay(delayMs);
155
+ var mid = (values.length >> 1);
156
+ var first = values.slice(0, mid);
157
+ var second = values.slice(mid);
158
+ await insertWithRetry(client, table, first, format, retries - 1 | 0);
159
+ return await insertWithRetry(client, table, second, format, retries - 1 | 0);
160
+ }
161
+ Logging.childWarn(logger, {
162
+ msg: "ClickHouse insert failed, retrying after delay",
163
+ table: table,
164
+ retriesLeft: retries,
165
+ err: Utils.prettifyExn(exn)
166
+ });
167
+ await Utils.delay(delayMs);
168
+ return await insertWithRetry(client, table, values, format, retries - 1 | 0);
169
+ }
170
+ throw exn;
171
+ }
172
+ }
173
+
79
174
  async function setCheckpointsOrThrow(client, batch, database) {
80
175
  var checkpointsCount = batch.checkpointIds.length;
81
176
  if (checkpointsCount === 0) {
@@ -84,7 +179,7 @@ async function setCheckpointsOrThrow(client, batch, database) {
84
179
  var checkpointRows = [];
85
180
  for(var idx = 0; idx < checkpointsCount; ++idx){
86
181
  checkpointRows.push([
87
- batch.checkpointIds[idx],
182
+ batch.checkpointIds[idx].toString(),
88
183
  batch.checkpointChainIds[idx],
89
184
  batch.checkpointBlockNumbers[idx],
90
185
  batch.checkpointBlockHashes[idx],
@@ -92,11 +187,7 @@ async function setCheckpointsOrThrow(client, batch, database) {
92
187
  ]);
93
188
  }
94
189
  try {
95
- return await client.insert({
96
- table: database + ".\`" + InternalTable.Checkpoints.table.tableName + "\`",
97
- values: checkpointRows,
98
- format: "JSONCompactEachRow"
99
- });
190
+ return await insertWithRetry(client, database + ".\`" + InternalTable.Checkpoints.table.tableName + "\`", checkpointRows, "JSONCompactEachRow", undefined);
100
191
  }
101
192
  catch (raw_exn){
102
193
  var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
@@ -109,18 +200,18 @@ async function setCheckpointsOrThrow(client, batch, database) {
109
200
  }
110
201
  }
111
202
 
112
- async function setUpdatesOrThrow(client, updates, entityConfig, database) {
203
+ async function setUpdatesOrThrow(client, cache, updates, entityConfig, database) {
113
204
  if (updates.length === 0) {
114
205
  return ;
115
206
  }
116
- var cache = entityConfig.clickHouseSetUpdatesCache;
207
+ var cached = cache.get(entityConfig);
117
208
  var match;
118
- if (cache !== undefined) {
119
- match = cache;
209
+ if (cached !== undefined) {
210
+ match = cached;
120
211
  } else {
121
- var cache_tableName = database + ".\`" + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + "\`";
122
- var cache_convertOrThrow = S$RescriptSchema.compile(S$RescriptSchema.union([
123
- EntityHistory.makeSetUpdateSchema(entityConfig.schema),
212
+ var cached_tableName = database + ".\`" + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + "\`";
213
+ var cached_convertOrThrow = S$RescriptSchema.compile(S$RescriptSchema.union([
214
+ EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
124
215
  S$RescriptSchema.object(function (s) {
125
216
  s.tag(EntityHistory.changeFieldName, "DELETE");
126
217
  return {
@@ -130,12 +221,12 @@ async function setUpdatesOrThrow(client, updates, entityConfig, database) {
130
221
  };
131
222
  })
132
223
  ]), "Output", "Json", "Sync", false);
133
- var cache$1 = {
134
- tableName: cache_tableName,
135
- convertOrThrow: cache_convertOrThrow
224
+ var cached$1 = {
225
+ tableName: cached_tableName,
226
+ convertOrThrow: cached_convertOrThrow
136
227
  };
137
- entityConfig.clickHouseSetUpdatesCache = cache$1;
138
- match = cache$1;
228
+ cache.set(entityConfig, cached$1);
229
+ match = cached$1;
139
230
  }
140
231
  var convertOrThrow = match.convertOrThrow;
141
232
  var tableName = match.tableName;
@@ -143,11 +234,7 @@ async function setUpdatesOrThrow(client, updates, entityConfig, database) {
143
234
  var values = updates.map(function (update) {
144
235
  return convertOrThrow(update.latestChange);
145
236
  });
146
- return await client.insert({
147
- table: tableName,
148
- values: values,
149
- format: "JSONEachRow"
150
- });
237
+ return await insertWithRetry(client, tableName, values, "JSONEachRow", undefined);
151
238
  }
152
239
  catch (raw_exn){
153
240
  var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
@@ -170,14 +257,14 @@ function makeCreateHistoryTableQuery(entityConfig, database) {
170
257
  var clickHouseType = getClickHouseFieldType(field$1.fieldType, field$1.isNullable, field$1.isArray);
171
258
  return "\`" + fieldName + "\` " + clickHouseType;
172
259
  }));
173
- return "CREATE TABLE IF NOT EXISTS " + database + ".\`" + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + "\` (\n " + fieldDefinitions.join(",\n ") + ",\n \`" + EntityHistory.checkpointIdFieldName + "\` " + getClickHouseFieldType("Uint32", false, false) + ",\n \`" + EntityHistory.changeFieldName + "\` " + getClickHouseFieldType({
260
+ return "CREATE TABLE IF NOT EXISTS " + database + ".\`" + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + "\` (\n " + fieldDefinitions.join(",\n ") + ",\n \`" + EntityHistory.checkpointIdFieldName + "\` " + getClickHouseFieldType("UInt64", false, false) + ",\n \`" + EntityHistory.changeFieldName + "\` " + getClickHouseFieldType({
174
261
  type: "Enum",
175
262
  config: EntityHistory.RowAction.config
176
263
  }, false, false) + "\n)\nENGINE = MergeTree()\nORDER BY (" + Table.idFieldName + ", " + EntityHistory.checkpointIdFieldName + ")";
177
264
  }
178
265
 
179
266
  function makeCreateCheckpointsTableQuery(database) {
180
- return "CREATE TABLE IF NOT EXISTS " + database + ".\`" + InternalTable.Checkpoints.table.tableName + "\` (\n \`" + "id" + "\` " + getClickHouseFieldType("Int32", false, false) + ",\n \`" + "chain_id" + "\` " + getClickHouseFieldType("Int32", false, false) + ",\n \`" + "block_number" + "\` " + getClickHouseFieldType("Int32", false, false) + ",\n \`" + "block_hash" + "\` " + getClickHouseFieldType("String", true, false) + ",\n \`" + "events_processed" + "\` " + getClickHouseFieldType("Int32", false, false) + "\n)\nENGINE = MergeTree()\nORDER BY (" + "id" + ")";
267
+ return "CREATE TABLE IF NOT EXISTS " + database + ".\`" + InternalTable.Checkpoints.table.tableName + "\` (\n \`" + "id" + "\` " + getClickHouseFieldType("UInt64", false, false) + ",\n \`" + "chain_id" + "\` " + getClickHouseFieldType("Int32", false, false) + ",\n \`" + "block_number" + "\` " + getClickHouseFieldType("Int32", false, false) + ",\n \`" + "block_hash" + "\` " + getClickHouseFieldType("String", true, false) + ",\n \`" + "events_processed" + "\` " + getClickHouseFieldType("UInt64", false, false) + "\n)\nENGINE = MergeTree()\nORDER BY (" + "id" + ")";
181
268
  }
182
269
 
183
270
  function makeCreateViewQuery(entityConfig, database) {
@@ -245,11 +332,11 @@ async function resume(client, database, checkpointId) {
245
332
  await Promise.all(Belt_Array.map(tables, (function (table) {
246
333
  var tableName = table.name;
247
334
  return client.exec({
248
- query: "ALTER TABLE " + database + ".\`" + tableName + "\` DELETE WHERE \`" + EntityHistory.checkpointIdFieldName + "\` > " + String(checkpointId)
335
+ query: "ALTER TABLE " + database + ".\`" + tableName + "\` DELETE WHERE \`" + EntityHistory.checkpointIdFieldName + "\` > " + checkpointId.toString()
249
336
  });
250
337
  })));
251
338
  return await client.exec({
252
- query: "DELETE FROM " + database + ".\`" + InternalTable.Checkpoints.table.tableName + "\` WHERE \`" + Table.idFieldName + "\` > " + String(checkpointId)
339
+ query: "DELETE FROM " + database + ".\`" + InternalTable.Checkpoints.table.tableName + "\` WHERE \`" + Table.idFieldName + "\` > " + checkpointId.toString()
253
340
  });
254
341
  }
255
342
  catch (raw_exn$1){
@@ -264,6 +351,9 @@ async function resume(client, database, checkpointId) {
264
351
 
265
352
  export {
266
353
  getClickHouseFieldType ,
354
+ makeClickHouseEntitySchema ,
355
+ logger ,
356
+ insertWithRetry ,
267
357
  setCheckpointsOrThrow ,
268
358
  setUpdatesOrThrow ,
269
359
  makeCreateHistoryTableQuery ,
@@ -272,4 +362,4 @@ export {
272
362
  initialize ,
273
363
  resume ,
274
364
  }
275
- /* Table Not a pure module */
365
+ /* logger Not a pure module */
@@ -0,0 +1,74 @@
1
+ /**
2
+ Formats:
3
+ hh:mm:ss | 00:00:00
4
+ do MMM ''yy | 1st Jan '21
5
+ ha do MMM ''yy | 8PM 1st Jan '21
6
+ ha | 8PM
7
+ iii | Tues
8
+ iii MMM | Tues Jan
9
+ MMM | Jan
10
+ `)
11
+ */
12
+ type dateFormats =
13
+ | @as("HH:mm:ss") HoursMinSec
14
+ | @as("ha") Hour
15
+ | @as("do MMM ''yy") DayMonthYear
16
+ | @as("ha do MMM ''yy") HourDayMonthYear
17
+ | @as("h:mma do MMM ''yy") HourMinDayMonthYear
18
+ | @as("iii") DayName
19
+ | @as("iii MMM") DayNameMonth
20
+ | @as("do MMM") DayMonth
21
+ | @as("MMM") Month
22
+ | @as("h:mma") HourMin
23
+
24
+ @module("date-fns") external format: (Js.Date.t, dateFormats) => string = "format"
25
+
26
+ type formatDistanceToNowOptions = {includeSeconds: bool}
27
+ @module("date-fns")
28
+ external formatDistanceToNow: Js.Date.t => string = "formatDistanceToNow"
29
+
30
+ @module("date-fns")
31
+ external formatDistance: (Js.Date.t, Js.Date.t) => string = "formatDistance"
32
+
33
+ @module("date-fns")
34
+ external formatDistanceWithOptions: (Js.Date.t, Js.Date.t, formatDistanceToNowOptions) => string =
35
+ "formatDistance"
36
+
37
+ @module("date-fns")
38
+ external formatDistanceToNowWithOptions: (Js.Date.t, formatDistanceToNowOptions) => string =
39
+ "formatDistanceToNow"
40
+
41
+ let formatDistanceToNowWithSeconds = (date: Js.Date.t) =>
42
+ date->formatDistanceToNowWithOptions({includeSeconds: true})
43
+
44
+ type durationTimeFormat = {
45
+ years: int,
46
+ months: int,
47
+ weeks: int,
48
+ days: int,
49
+ hours: int,
50
+ minutes: int,
51
+ seconds: int,
52
+ }
53
+
54
+ @module("date-fns")
55
+ external formatRelative: (Js.Date.t, Js.Date.t) => string = "formatRelative"
56
+
57
+ type durationFormatOutput = {format: array<string>}
58
+
59
+ @module("date-fns")
60
+ external formatDuration: (durationTimeFormat, durationFormatOutput) => string = "formatDuration"
61
+
62
+ type interval = {start: Js_date.t, end: Js_date.t}
63
+
64
+ @module("date-fns")
65
+ external intervalToDuration: interval => durationTimeFormat = "intervalToDuration"
66
+
67
+ //helper to convert millis elapsed to duration object
68
+ let durationFromMillis = (millis: int) =>
69
+ intervalToDuration({
70
+ start: 0->(Utils.magic: int => Js_date.t),
71
+ end: millis->(Utils.magic: int => Js_date.t),
72
+ })
73
+
74
+ @module("date-fns") external fromUnixTime: float => Js.Date.t = "fromUnixTime"
@@ -0,0 +1,22 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as DateFns from "date-fns";
4
+
5
+ function formatDistanceToNowWithSeconds(date) {
6
+ return DateFns.formatDistanceToNow(date, {
7
+ includeSeconds: true
8
+ });
9
+ }
10
+
11
+ function durationFromMillis(millis) {
12
+ return DateFns.intervalToDuration({
13
+ start: 0,
14
+ end: millis
15
+ });
16
+ }
17
+
18
+ export {
19
+ formatDistanceToNowWithSeconds ,
20
+ durationFromMillis ,
21
+ }
22
+ /* date-fns Not a pure module */
@@ -1,6 +1,13 @@
1
1
  type t
2
2
 
3
- type options = {headers?: Js.Dict.t<string>}
3
+ module Fetch = {
4
+ type args = {body?: unknown, headers?: dict<string>, method?: string, path?: string}
5
+ type t = (string, ~args: args) => promise<unknown>
6
+ // NOTE: don't try make the type t. Rescript 11 will curry the args which breaks
7
+ // keet the type inline. This is a workaround for now.
8
+ external fetch: (string, ~args: args) => promise<unknown> = "fetch"
9
+ }
10
+ type options = {fetch?: Fetch.t}
4
11
 
5
12
  @module("eventsource") @new
6
13
  external create: (~url: string, ~options: options=?) => t = "EventSource"
@@ -1,2 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
- /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
2
+
3
+
4
+ var Fetch = {};
5
+
6
+ export {
7
+ Fetch ,
8
+ }
9
+ /* No side effect */
@@ -28,6 +28,7 @@ type middleware = (req, res, unit => unit) => unit
28
28
  type server
29
29
 
30
30
  @send external listen: (app, int) => server = "listen"
31
+ @send external onError: (server, @as("error") _, Js.Exn.t => unit) => unit = "on"
31
32
 
32
33
  // res methods
33
34
  @send external sendStatus: (res, int) => unit = "sendStatus"
@@ -34,12 +34,25 @@ let toMillis = ((sec, nano): timeElapsed): milliseconds => {
34
34
  sec->secToMilli +. nano->nanoToMilli
35
35
  }
36
36
 
37
+ let toSeconds = ((sec, nano): timeElapsed): seconds => {
38
+ sec +. nano /. 1_000_000_000.
39
+ }
40
+
41
+ let toSecondsFloat = (elapsed: timeElapsed): float => {
42
+ elapsed->toSeconds->(Utils.magic: seconds => float)
43
+ }
44
+
37
45
  let toInt = float => float->Belt.Int.fromFloat
38
46
  let intFromMillis = toInt
39
47
  let intFromNanos = toInt
40
48
  let intFromSeconds = toInt
41
- let floatFromMillis = Utils.magic
49
+ let floatFromMillis: milliseconds => float = v => v->(Utils.magic: milliseconds => float)
50
+ let floatFromSeconds: seconds => float = v => v->(Utils.magic: seconds => float)
42
51
 
43
52
  let millisBetween = (~from: timeRef, ~to: timeRef): int => {
44
53
  to->toMillis->intFromMillis - from->toMillis->intFromMillis
45
54
  }
55
+
56
+ let secondsBetween = (~from: timeRef, ~to: timeRef): float => {
57
+ to->toSecondsFloat -. from->toSecondsFloat
58
+ }