envio 3.9.0 → 3.10.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 (110) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +35 -3
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +52 -44
  10. package/src/Config.res.mjs +51 -17
  11. package/src/Core.res +4 -0
  12. package/src/CrossChainState.res +15 -23
  13. package/src/CrossChainState.res.mjs +10 -18
  14. package/src/CrossChainState.resi +4 -1
  15. package/src/Ecosystem.res +7 -2
  16. package/src/Envio.res +6 -4
  17. package/src/EventConfigBuilder.res +169 -26
  18. package/src/EventConfigBuilder.res.mjs +124 -15
  19. package/src/HandlerRegister.res +21 -4
  20. package/src/HandlerRegister.res.mjs +24 -5
  21. package/src/InMemoryStore.res +9 -7
  22. package/src/InMemoryStore.res.mjs +3 -3
  23. package/src/IndexerState.res +43 -6
  24. package/src/IndexerState.res.mjs +36 -7
  25. package/src/IndexerState.resi +4 -4
  26. package/src/Internal.res +16 -17
  27. package/src/Metrics.res +72 -6
  28. package/src/Metrics.res.mjs +30 -2
  29. package/src/Persistence.res +36 -30
  30. package/src/Persistence.res.mjs +1 -20
  31. package/src/PgStorage.res +104 -56
  32. package/src/PgStorage.res.mjs +57 -35
  33. package/src/PruneStaleHistory.res +85 -52
  34. package/src/PruneStaleHistory.res.mjs +63 -37
  35. package/src/Rollback.res +68 -45
  36. package/src/Rollback.res.mjs +38 -19
  37. package/src/RollbackCommit.res +7 -9
  38. package/src/RollbackCommit.res.mjs +5 -6
  39. package/src/SafeCheckpointTracking.res +11 -11
  40. package/src/SafeCheckpointTracking.res.mjs +7 -6
  41. package/src/SimulateItems.res +33 -18
  42. package/src/SimulateItems.res.mjs +26 -22
  43. package/src/Sink.res +38 -26
  44. package/src/Sink.res.mjs +21 -16
  45. package/src/TestIndexer.res +5 -5
  46. package/src/TestIndexer.res.mjs +1 -1
  47. package/src/Utils.res +170 -0
  48. package/src/Utils.res.mjs +153 -0
  49. package/src/Writing.res +2 -2
  50. package/src/Writing.res.mjs +1 -1
  51. package/src/bindings/ClickHouse.res +366 -698
  52. package/src/bindings/ClickHouse.res.mjs +371 -421
  53. package/src/bindings/ClickHouseSink.res +337 -0
  54. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  55. package/src/bindings/EventSource.res +8 -2
  56. package/src/bindings/NodeJs.res +6 -0
  57. package/src/bindings/Vitest.res +23 -0
  58. package/src/bindings/Vitest.res.mjs +3 -0
  59. package/src/bindings/WebSocket.res +9 -10
  60. package/src/db/CheckpointBounds.res +53 -0
  61. package/src/db/CheckpointBounds.res.mjs +44 -0
  62. package/src/db/EntityFilter.res +36 -17
  63. package/src/db/EntityFilter.res.mjs +25 -14
  64. package/src/db/EntityHistory.res +47 -22
  65. package/src/db/EntityHistory.res.mjs +19 -12
  66. package/src/db/InternalTable.res +84 -35
  67. package/src/db/InternalTable.res.mjs +56 -23
  68. package/src/db/RollbackFloors.res +55 -0
  69. package/src/db/RollbackFloors.res.mjs +92 -0
  70. package/src/db/Table.res +22 -4
  71. package/src/db/Table.res.mjs +30 -9
  72. package/src/sources/Evm.res +2 -0
  73. package/src/sources/Evm.res.mjs +2 -0
  74. package/src/sources/EvmHyperSyncSource.res +3 -3
  75. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  76. package/src/sources/EvmRpcWs.res +124 -0
  77. package/src/sources/EvmRpcWs.res.mjs +117 -0
  78. package/src/sources/Fuel.res +2 -0
  79. package/src/sources/Fuel.res.mjs +2 -0
  80. package/src/sources/FuelHyperSyncSource.res +1 -1
  81. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  82. package/src/sources/HeightFeed.res +568 -0
  83. package/src/sources/HeightFeed.res.mjs +452 -0
  84. package/src/sources/HeightStream.res +257 -0
  85. package/src/sources/HeightStream.res.mjs +194 -0
  86. package/src/sources/HyperSync.res +5 -0
  87. package/src/sources/HyperSync.res.mjs +3 -0
  88. package/src/sources/HyperSync.resi +4 -0
  89. package/src/sources/HyperSyncSSE.res +49 -0
  90. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  91. package/src/sources/RpcSource.res +1 -1
  92. package/src/sources/RpcSource.res.mjs +2 -2
  93. package/src/sources/Source.res +54 -1
  94. package/src/sources/Source.res.mjs +25 -0
  95. package/src/sources/SourceManager.res +220 -227
  96. package/src/sources/SourceManager.res.mjs +139 -164
  97. package/src/sources/SourceManager.resi +8 -0
  98. package/src/sources/Svm.res +2 -0
  99. package/src/sources/Svm.res.mjs +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +70 -127
  101. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  102. package/src/sources/SvmHyperSyncSource.res +18 -15
  103. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  104. package/svm.schema.json +1 -75
  105. package/src/MemoryStorage.res +0 -729
  106. package/src/MemoryStorage.res.mjs +0 -586
  107. package/src/sources/HyperSyncHeightStream.res +0 -129
  108. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  109. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  110. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -9,6 +9,7 @@ import * as AddressRows from "../AddressRows.res.mjs";
9
9
  import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
10
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
11
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
12
+ import * as CheckpointBounds from "./CheckpointBounds.res.mjs";
12
13
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
13
14
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
14
15
 
@@ -389,13 +390,35 @@ let dbSchema = S$RescriptSchema.object(s => ({
389
390
  events_processed: s.f("events_processed", S$RescriptSchema.int)
390
391
  }));
391
392
 
392
- let table$4 = Table.mkTable("envio_checkpoints", undefined, [
393
- Table.mkField("id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
394
- Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
395
- Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
396
- Table.mkField("block_hash", "String", S$RescriptSchema.$$null(S$RescriptSchema.string), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
397
- Table.mkField("events_processed", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
398
- ], undefined);
393
+ let columns = [
394
+ {
395
+ field: Table.mkField("id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
396
+ clickHouseFieldType: "UInt64",
397
+ valuesOf: batch => batch.checkpointIds
398
+ },
399
+ {
400
+ field: Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
401
+ clickHouseFieldType: "ChainId",
402
+ valuesOf: batch => batch.checkpointChainIds
403
+ },
404
+ {
405
+ field: Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
406
+ clickHouseFieldType: "Int32",
407
+ valuesOf: batch => batch.checkpointBlockNumbers
408
+ },
409
+ {
410
+ field: Table.mkField("block_hash", "String", S$RescriptSchema.$$null(S$RescriptSchema.string), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
411
+ clickHouseFieldType: "String",
412
+ valuesOf: batch => batch.checkpointBlockHashes
413
+ },
414
+ {
415
+ field: Table.mkField("events_processed", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
416
+ clickHouseFieldType: "UInt64",
417
+ valuesOf: batch => batch.checkpointEventsProcessed
418
+ }
419
+ ];
420
+
421
+ let table$4 = Table.mkTable("envio_checkpoints", undefined, columns.map(param => param.field), undefined);
399
422
 
400
423
  function makeGetReorgCheckpointsQuery(pgSchema) {
401
424
  return `WITH reorg_chains AS (
@@ -415,7 +438,8 @@ FROM "` + pgSchema + `"."` + table$4.tableName + `" cp
415
438
  INNER JOIN reorg_chains rc
416
439
  ON cp."` + "chain_id" + `" = rc.id
417
440
  WHERE cp."` + "block_hash" + `" IS NOT NULL
418
- AND cp."` + "block_number" + `" >= rc.safe_block;`;
441
+ AND cp."` + "block_number" + `" >= rc.safe_block
442
+ ORDER BY cp."` + "id" + `";`;
419
443
  }
420
444
 
421
445
  function makeCommitedCheckpointIdQuery(pgSchema) {
@@ -442,16 +466,22 @@ function insert$2(sql, pgSchema, checkpointIds, checkpointChainIds, checkpointBl
442
466
  ], {prepare: true});
443
467
  }
444
468
 
445
- function rollback(sql, pgSchema, rollbackTargetCheckpointId) {
446
- return sql.unsafe(`DELETE FROM "` + pgSchema + `"."` + table$4.tableName + `" WHERE "` + "id" + `" > $1;`, [rollbackTargetCheckpointId.toString()], {prepare: true});
469
+ let chainIdColumn = "chain_id";
470
+
471
+ function rollback(sql, pgSchema, floors) {
472
+ let tableRef = `"` + pgSchema + `"."` + table$4.tableName + `"`;
473
+ let bounds = CheckpointBounds.sql(floors.floors, chainIdColumn, tableRef);
474
+ return sql.unsafe(`DELETE FROM ` + tableRef + bounds.using + ` WHERE "` + "id" + `" > ` + bounds.checkpointId + bounds.usingMatch + `;`, CheckpointBounds.params(floors.floors), {prepare: true});
447
475
  }
448
476
 
449
- function makePruneStaleCheckpointsQuery(pgSchema) {
450
- return `DELETE FROM "` + pgSchema + `"."` + table$4.tableName + `" WHERE "` + "id" + `" < $1;`;
477
+ function makePruneStaleCheckpointsQuery(pgSchema, safeCheckpoints) {
478
+ let tableRef = `"` + pgSchema + `"."` + table$4.tableName + `"`;
479
+ let bounds = CheckpointBounds.sql(safeCheckpoints, chainIdColumn, tableRef);
480
+ return `DELETE FROM ` + tableRef + bounds.using + ` WHERE "` + "id" + `" < ` + bounds.checkpointId + bounds.usingMatch + `;`;
451
481
  }
452
482
 
453
- function pruneStaleCheckpoints(sql, pgSchema, safeCheckpointId) {
454
- return sql.unsafe(makePruneStaleCheckpointsQuery(pgSchema), [safeCheckpointId.toString()], {prepare: true});
483
+ function pruneStaleCheckpoints(sql, pgSchema, safeCheckpoints) {
484
+ return sql.unsafe(makePruneStaleCheckpointsQuery(pgSchema, safeCheckpoints), CheckpointBounds.params(safeCheckpoints), {prepare: true});
455
485
  }
456
486
 
457
487
  function makeGetRollbackTargetCheckpointQuery(pgSchema) {
@@ -471,28 +501,31 @@ function getRollbackTargetCheckpoint(sql, pgSchema, reorgChainId, lastKnownValid
471
501
  return rawResult.then(rows => Stdlib_Option.map(rows[0], row => BigInt(row.id)));
472
502
  }
473
503
 
474
- function makeGetRollbackProgressDiffQuery(pgSchema) {
504
+ function makeGetRollbackProgressDiffQuery(pgSchema, floors) {
505
+ let bounds = CheckpointBounds.sql(floors.floors, chainIdColumn, "t");
475
506
  return `SELECT
476
- "` + "chain_id" + `"::float8 as "` + "chain_id" + `",
477
- SUM("` + "events_processed" + `") as events_processed_diff,
478
- MIN("` + "block_number" + `") - 1 as new_progress_block_number
479
- FROM "` + pgSchema + `"."` + table$4.tableName + `"
480
- WHERE "` + "id" + `" > $1
481
- GROUP BY "` + "chain_id" + `";`;
507
+ t."` + "chain_id" + `"::float8 as "` + "chain_id" + `",
508
+ SUM(t."` + "events_processed" + `") as events_processed_diff,
509
+ MIN(t."` + "block_number" + `") - 1 as new_progress_block_number
510
+ FROM "` + pgSchema + `"."` + table$4.tableName + `" t` + bounds.join + `
511
+ WHERE t."` + "id" + `" > ` + bounds.checkpointId + `
512
+ GROUP BY t."` + "chain_id" + `";`;
482
513
  }
483
514
 
484
- function getRollbackProgressDiff(sql, pgSchema, rollbackTargetCheckpointId) {
485
- return sql.unsafe(makeGetRollbackProgressDiffQuery(pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true});
515
+ function getRollbackProgressDiff(sql, pgSchema, floors) {
516
+ return sql.unsafe(makeGetRollbackProgressDiffQuery(pgSchema, floors), CheckpointBounds.params(floors.floors), {prepare: true});
486
517
  }
487
518
 
488
519
  let Checkpoints = {
489
520
  dbSchema: dbSchema,
490
521
  initialCheckpointId: 0n,
522
+ columns: columns,
491
523
  table: table$4,
492
524
  makeGetReorgCheckpointsQuery: makeGetReorgCheckpointsQuery,
493
525
  makeCommitedCheckpointIdQuery: makeCommitedCheckpointIdQuery,
494
526
  makeInsertCheckpointQuery: makeInsertCheckpointQuery,
495
527
  insert: insert$2,
528
+ chainIdColumn: chainIdColumn,
496
529
  rollback: rollback,
497
530
  makePruneStaleCheckpointsQuery: makePruneStaleCheckpointsQuery,
498
531
  pruneStaleCheckpoints: pruneStaleCheckpoints,
@@ -0,0 +1,55 @@
1
+ // How far back a reorg rollback takes each chain: the last checkpoint each keeps,
2
+ // and for a chain whose own reorg set its floor, the last block that reorg left
3
+ // valid. Superseding an unwritten rollback with a new one is a pointwise
4
+ // minimum, so a merge can neither lose a chain's floor nor raise it.
5
+ type t = {
6
+ floors: CheckpointBounds.t,
7
+ // Progress recomputed from the surviving checkpoints alone can land above the
8
+ // fork: the blocks between it and the chain's next checkpoint carried no
9
+ // events on the orphaned chain, but the chain replacing it can have its own.
10
+ // Absent for a chain dragged along by another chain's reorg.
11
+ forkBlockNumberByChain: array<(ChainId.t, int)>,
12
+ }
13
+
14
+ // The rollback of a schema whose chains can't have written each other's rows.
15
+ let isolated = (~chainId, ~floorCheckpointId, ~forkBlockNumber) => {
16
+ floors: PerChain([(chainId, floorCheckpointId)]),
17
+ forkBlockNumberByChain: [(chainId, forkBlockNumber)],
18
+ }
19
+
20
+ // The rollback of a schema with a cross-chain entity: every chain goes back to
21
+ // the same checkpoint.
22
+ let global = (~floorCheckpointId, ~reorgChainId, ~forkBlockNumber) => {
23
+ floors: EveryChain(floorCheckpointId),
24
+ forkBlockNumberByChain: [(reorgChainId, forkBlockNumber)],
25
+ }
26
+
27
+ let mergeByChain = (pending: array<(ChainId.t, 'a)>, next: array<(ChainId.t, 'a)>) => {
28
+ let merged = pending->Array.copy
29
+ next->Array.forEach(((chainId, value)) =>
30
+ switch merged->Array.findIndexOpt(((mergedChainId, _)) => mergedChainId === chainId) {
31
+ | Some(idx) =>
32
+ let (_, pendingValue) = merged->Array.getUnsafe(idx)
33
+ merged->Array.setUnsafe(idx, (chainId, Pervasives.min(pendingValue, value)))
34
+ | None => merged->Array.push((chainId, value))
35
+ }
36
+ )
37
+ merged
38
+ }
39
+
40
+ let merge = (pending: t, next: t) => {
41
+ floors: switch (pending.floors, next.floors) {
42
+ | (EveryChain(pending), EveryChain(next)) => EveryChain(Pervasives.min(pending, next))
43
+ | (PerChain(pending), PerChain(next)) => PerChain(mergeByChain(pending, next))
44
+ | (EveryChain(_), PerChain(_)) | (PerChain(_), EveryChain(_)) =>
45
+ JsError.throwWithMessage(
46
+ "Internal error: a rollback reaching every chain can't be merged with one isolated to a chain. The schema decides which a run builds, and it doesn't change.",
47
+ )
48
+ },
49
+ forkBlockNumberByChain: mergeByChain(pending.forkBlockNumberByChain, next.forkBlockNumberByChain),
50
+ }
51
+
52
+ let forkBlockNumber = (floors: t, chainId) =>
53
+ floors.forkBlockNumberByChain
54
+ ->Array.find(((forkChainId, _)) => forkChainId === chainId)
55
+ ->Option.map(((_, forkBlockNumber)) => forkBlockNumber)
@@ -0,0 +1,92 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Primitive_bigint from "@rescript/runtime/lib/es6/Primitive_bigint.js";
7
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+
9
+ function isolated(chainId, floorCheckpointId, forkBlockNumber) {
10
+ return {
11
+ floors: {
12
+ TAG: "PerChain",
13
+ _0: [[
14
+ chainId,
15
+ floorCheckpointId
16
+ ]]
17
+ },
18
+ forkBlockNumberByChain: [[
19
+ chainId,
20
+ forkBlockNumber
21
+ ]]
22
+ };
23
+ }
24
+
25
+ function global(floorCheckpointId, reorgChainId, forkBlockNumber) {
26
+ return {
27
+ floors: {
28
+ TAG: "EveryChain",
29
+ _0: floorCheckpointId
30
+ },
31
+ forkBlockNumberByChain: [[
32
+ reorgChainId,
33
+ forkBlockNumber
34
+ ]]
35
+ };
36
+ }
37
+
38
+ function mergeByChain(pending, next) {
39
+ let merged = pending.slice();
40
+ next.forEach(param => {
41
+ let value = param[1];
42
+ let chainId = param[0];
43
+ let idx = Stdlib_Array.findIndexOpt(merged, param => param[0] === chainId);
44
+ if (idx !== undefined) {
45
+ let match = merged[idx];
46
+ merged[idx] = [
47
+ chainId,
48
+ Primitive_object.min(match[1], value)
49
+ ];
50
+ return;
51
+ }
52
+ merged.push([
53
+ chainId,
54
+ value
55
+ ]);
56
+ });
57
+ return merged;
58
+ }
59
+
60
+ function merge(pending, next) {
61
+ let match = pending.floors;
62
+ let match$1 = next.floors;
63
+ let tmp;
64
+ tmp = match.TAG === "EveryChain" ? (
65
+ match$1.TAG === "EveryChain" ? ({
66
+ TAG: "EveryChain",
67
+ _0: Primitive_bigint.min(match._0, match$1._0)
68
+ }) : Stdlib_JsError.throwWithMessage("Internal error: a rollback reaching every chain can't be merged with one isolated to a chain. The schema decides which a run builds, and it doesn't change.")
69
+ ) : (
70
+ match$1.TAG === "EveryChain" ? Stdlib_JsError.throwWithMessage("Internal error: a rollback reaching every chain can't be merged with one isolated to a chain. The schema decides which a run builds, and it doesn't change.") : ({
71
+ TAG: "PerChain",
72
+ _0: mergeByChain(match._0, match$1._0)
73
+ })
74
+ );
75
+ return {
76
+ floors: tmp,
77
+ forkBlockNumberByChain: mergeByChain(pending.forkBlockNumberByChain, next.forkBlockNumberByChain)
78
+ };
79
+ }
80
+
81
+ function forkBlockNumber(floors, chainId) {
82
+ return Stdlib_Option.map(floors.forkBlockNumberByChain.find(param => param[0] === chainId), param => param[1]);
83
+ }
84
+
85
+ export {
86
+ isolated,
87
+ global,
88
+ mergeByChain,
89
+ merge,
90
+ forkBlockNumber,
91
+ }
92
+ /* No side effect */
package/src/db/Table.res CHANGED
@@ -22,8 +22,9 @@ type fieldType =
22
22
  | Uint32
23
23
  | UInt52
24
24
  | SmallInt
25
- // Raw bytes. Only the internal tables use it the one column that stores an
26
- // address in the binary form the Rust address store keys on.
25
+ // Raw bytes: the `Bytes` scalar under `bytes_type: uint8array`, and the
26
+ // internal column that stores an address in the binary form the Rust address
27
+ // store keys on.
27
28
  | Bytea
28
29
  | UInt64
29
30
  | Int32
@@ -288,6 +289,9 @@ let getChainIdField = (table): option<field> =>
288
289
  }
289
290
  )
290
291
 
292
+ // None for a cross-chain entity, whose rows no single chain owns.
293
+ let getPgChainIdColumn = table => table->getChainIdField->Option.map(getPgDbFieldName)
294
+
291
295
  let getFieldByName = (table, fieldName) =>
292
296
  table.fields->Array.find(field => field->getUserDefinedFieldName === fieldName)
293
297
 
@@ -360,7 +364,10 @@ let queryFields: table => dict<queryField> = Utils.WeakMap.memoize(table => {
360
364
  field->getApiFieldName,
361
365
  {
362
366
  fieldSchema: field.fieldSchema,
363
- arrayFieldSchema: S.array(field.fieldSchema)->S.toUnknown,
367
+ arrayFieldSchema: switch field.fieldType {
368
+ | Bytea => Utils.Schema.bytesArray->S.toUnknown
369
+ | _ => S.array(field.fieldSchema)->S.toUnknown
370
+ },
364
371
  pgDbFieldName: field->getPgDbFieldName,
365
372
  isChainId: field.isChainId,
366
373
  },
@@ -431,6 +438,7 @@ type sqlParams<'entity> = {
431
438
  quotedFieldNames: array<string>,
432
439
  quotedNonPrimaryFieldNames: array<string>,
433
440
  arrayFieldTypes: array<string>,
441
+ byteaColumnIndexes: array<int>,
434
442
  hasArrayField: bool,
435
443
  }
436
444
 
@@ -438,6 +446,9 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
438
446
  let quotedFieldNames = []
439
447
  let quotedNonPrimaryFieldNames = []
440
448
  let arrayFieldTypes = []
449
+ // Positions of the bytea columns among the unnest parameters, which the
450
+ // caller binds as array literals (see `Utils.Bytes.toPgArrayLiteral`).
451
+ let byteaColumnIndexes = []
441
452
  let hasArrayField = ref(false)
442
453
 
443
454
  let dbSchema: S.t<dict<unknown>> = S.schema(s =>
@@ -450,7 +461,7 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
450
461
  | BigInt => Utils.BigInt.schema->S.toUnknown
451
462
  | Option(child)
452
463
  | Null(child) =>
453
- S.null(child->coerceSchema)->S.toUnknown
464
+ Utils.Schema.nullTolerant(child->coerceSchema)->S.toUnknown
454
465
  | Array(child) => {
455
466
  hasArrayField := true
456
467
  S.array(child->coerceSchema)->S.toUnknown
@@ -472,6 +483,12 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
472
483
  | Some(field) => field
473
484
  | None => throw(NonExistingTableField(location))
474
485
  }
486
+ switch field {
487
+ | Field({isArray: true}) => hasArrayField := true
488
+ | Field({fieldType: Bytea}) =>
489
+ byteaColumnIndexes->Array.push(arrayFieldTypes->Array.length)->ignore
490
+ | _ => ()
491
+ }
475
492
 
476
493
  // Schema locations use API field names, while the SQL references
477
494
  // columns by their possibly renamed db names.
@@ -521,6 +538,7 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
521
538
  quotedFieldNames,
522
539
  quotedNonPrimaryFieldNames,
523
540
  arrayFieldTypes,
541
+ byteaColumnIndexes,
524
542
  hasArrayField: hasArrayField.contents,
525
543
  }
526
544
  }
@@ -253,6 +253,10 @@ function getChainIdField(table) {
253
253
  });
254
254
  }
255
255
 
256
+ function getPgChainIdColumn(table) {
257
+ return Stdlib_Option.map(getChainIdField(table), getPgDbFieldName);
258
+ }
259
+
256
260
  function getFieldByName(table, fieldName) {
257
261
  return table.fields.find(field => getUserDefinedFieldName(field) === fieldName);
258
262
  }
@@ -307,9 +311,12 @@ let queryFields = Utils.$$WeakMap.memoize(table => {
307
311
  return;
308
312
  }
309
313
  let field$1 = field._0;
314
+ let match = field$1.fieldType;
315
+ let tmp;
316
+ tmp = typeof match !== "object" && match === "Bytea" ? Utils.Schema.bytesArray : S$RescriptSchema.array(field$1.fieldSchema);
310
317
  dict[getApiFieldName(field$1)] = {
311
318
  fieldSchema: field$1.fieldSchema,
312
- arrayFieldSchema: S$RescriptSchema.array(field$1.fieldSchema),
319
+ arrayFieldSchema: tmp,
313
320
  pgDbFieldName: getPgDbFieldName(field$1),
314
321
  isChainId: field$1.isChainId
315
322
  };
@@ -368,6 +375,7 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
368
375
  let quotedFieldNames = [];
369
376
  let quotedNonPrimaryFieldNames = [];
370
377
  let arrayFieldTypes = [];
378
+ let byteaColumnIndexes = [];
371
379
  let hasArrayField = {
372
380
  contents: false
373
381
  };
@@ -400,7 +408,7 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
400
408
  switch (child.TAG) {
401
409
  case "option" :
402
410
  case "null" :
403
- return S$RescriptSchema.$$null(coerceSchema(child._0));
411
+ return Utils.Schema.nullTolerant(coerceSchema(child._0));
404
412
  case "array" :
405
413
  hasArrayField.contents = true;
406
414
  return S$RescriptSchema.array(coerceSchema(child._0));
@@ -423,6 +431,17 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
423
431
  Error: new Error()
424
432
  };
425
433
  }
434
+ if (field$1.TAG === "Field") {
435
+ let match = field$1._0;
436
+ if (match.isArray) {
437
+ hasArrayField.contents = true;
438
+ } else {
439
+ let tmp = match.fieldType;
440
+ if (typeof tmp !== "object" && tmp === "Bytea") {
441
+ byteaColumnIndexes.push(arrayFieldTypes.length);
442
+ }
443
+ }
444
+ }
426
445
  let quotedDbName = `"` + getPgFieldName(field$1) + `"`;
427
446
  quotedFieldNames.push(quotedDbName);
428
447
  if (field$1.TAG === "Field") {
@@ -432,20 +451,20 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
432
451
  quotedNonPrimaryFieldNames.push(quotedDbName);
433
452
  }
434
453
  }
435
- let tmp;
454
+ let tmp$1;
436
455
  if (field$1.TAG === "Field") {
437
456
  let f = field$1._0;
438
457
  let pgFieldType = getPgFieldType(f.fieldType, pgSchema, true, false, f.isNullable, chainIdMode);
439
- let match = f.fieldType;
440
- tmp = typeof match !== "object" ? (
441
- match === "Boolean" ? "INTEGER" + `[]::` + pgFieldType : pgFieldType
458
+ let match$1 = f.fieldType;
459
+ tmp$1 = typeof match$1 !== "object" ? (
460
+ match$1 === "Boolean" ? "INTEGER" + `[]::` + pgFieldType : pgFieldType
442
461
  ) : (
443
- match.type === "Enum" ? "TEXT" + `[]::` + pgFieldType : pgFieldType
462
+ match$1.type === "Enum" ? "TEXT" + `[]::` + pgFieldType : pgFieldType
444
463
  );
445
464
  } else {
446
- tmp = "TEXT[]";
465
+ tmp$1 = "TEXT[]";
447
466
  }
448
- arrayFieldTypes.push(tmp);
467
+ arrayFieldTypes.push(tmp$1);
449
468
  dict[location] = s.m(coerceSchema(param.schema));
450
469
  });
451
470
  return dict;
@@ -455,6 +474,7 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
455
474
  quotedFieldNames: quotedFieldNames,
456
475
  quotedNonPrimaryFieldNames: quotedNonPrimaryFieldNames,
457
476
  arrayFieldTypes: arrayFieldTypes,
477
+ byteaColumnIndexes: byteaColumnIndexes,
458
478
  hasArrayField: hasArrayField.contents
459
479
  };
460
480
  }
@@ -505,6 +525,7 @@ export {
505
525
  getLinkedEntityFields,
506
526
  getDerivedFromFields,
507
527
  getChainIdField,
528
+ getPgChainIdColumn,
508
529
  getFieldByName,
509
530
  NoIdField,
510
531
  getIdFieldOrThrow,
@@ -77,6 +77,8 @@ let make = (~logger: Pino.t): Ecosystem.t => {
77
77
  blockTimestampName: "timestamp",
78
78
  blockHashName: "hash",
79
79
  onBlockMethodName: "onBlock",
80
+ contractNoun: "contract",
81
+ eventNoun: "event",
80
82
  // EVM filter shape: `{block: {number: {_gte?, _lte?, _every?}}}`.
81
83
  // The inner range chunk is returned as raw `S.unknown` and parsed a
82
84
  // second time in `Main.res` by the shared `blockRangeSchema`.
@@ -56,6 +56,8 @@ function make(logger) {
56
56
  blockTimestampName: "timestamp",
57
57
  blockHashName: "hash",
58
58
  onBlockMethodName: "onBlock",
59
+ contractNoun: "contract",
60
+ eventNoun: "event",
59
61
  onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
60
62
  onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
61
63
  logger: logger,
@@ -205,7 +205,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
205
205
  name,
206
206
  sourceFor: Sync,
207
207
  chainId,
208
- pollingInterval: 100,
208
+ pollingInterval: HyperSync.pollingInterval,
209
209
  poweredByHyperSync: true,
210
210
  getBlockHashes,
211
211
  getHeightOrThrow: async () => {
@@ -227,7 +227,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
227
227
  {height, requestStats: [{method: "getHeight", seconds}]}
228
228
  },
229
229
  getItemsOrThrow,
230
- createHeightSubscription: (~onHeight) =>
231
- HyperSyncHeightStream.subscribe(~hyperSyncUrl=endpointUrl, ~apiToken, ~chainId, ~onHeight),
230
+ createHeightSubscription: (~onHeight, ~onStatus) =>
231
+ HyperSyncSSE.subscribe(~hyperSyncUrl=endpointUrl, ~apiToken, ~onHeight, ~onStatus),
232
232
  }
233
233
  }
@@ -4,6 +4,7 @@ import * as Source from "./Source.res.mjs";
4
4
  import * as Logging from "../Logging.res.mjs";
5
5
  import * as HyperSync from "./HyperSync.res.mjs";
6
6
  import * as Performance from "../bindings/Performance.res.mjs";
7
+ import * as HyperSyncSSE from "./HyperSyncSSE.res.mjs";
7
8
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
8
9
  import * as ErrorHandling from "../ErrorHandling.res.mjs";
9
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
@@ -11,7 +12,6 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
11
12
  import * as HyperSyncClient from "./HyperSyncClient.res.mjs";
12
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
13
14
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
14
- import * as HyperSyncHeightStream from "./HyperSyncHeightStream.res.mjs";
15
15
 
16
16
  function isUnauthorizedError(message) {
17
17
  return message.includes("401 Unauthorized");
@@ -143,7 +143,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
143
143
  sourceFor: "Sync",
144
144
  chainId: chainId,
145
145
  poweredByHyperSync: true,
146
- pollingInterval: 100,
146
+ pollingInterval: HyperSync.pollingInterval,
147
147
  getBlockHashes: getBlockHashes,
148
148
  getHeightOrThrow: async () => {
149
149
  let timerRef = Performance.now();
@@ -188,7 +188,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
188
188
  };
189
189
  },
190
190
  getItemsOrThrow: getItemsOrThrow,
191
- createHeightSubscription: onHeight => HyperSyncHeightStream.subscribe(endpointUrl, apiToken$1, chainId, onHeight)
191
+ createHeightSubscription: (onHeight, onStatus) => HyperSyncSSE.subscribe(endpointUrl, apiToken$1, onHeight, onStatus)
192
192
  };
193
193
  }
194
194
 
@@ -0,0 +1,124 @@
1
+ // newHeads carries no keep-alive, so this has to tolerate slow chains rather
2
+ // than the seconds an SSE ping allows.
3
+ let staleTimeout = 60_000
4
+
5
+ // The id of the one request this stream ever sends, which is what tells a
6
+ // response meant for it apart from one about anything else sharing the socket.
7
+ let subscribeRequestId = 1
8
+ let subscribeRequestIdText = subscribeRequestId->Int.toString
9
+
10
+ type wsMessage =
11
+ | NewHead(int)
12
+ // A response to some request, carrying the id it answers when it names one.
13
+ | Response({id: option<string>, isError: bool})
14
+
15
+ let subscribeRequestJson =
16
+ {"jsonrpc": "2.0", "id": subscribeRequestId, "method": "eth_subscribe", "params": ["newHeads"]}
17
+ ->(Utils.magic: {
18
+ "jsonrpc": string,
19
+ "id": int,
20
+ "method": string,
21
+ "params": array<string>,
22
+ } => JSON.t)
23
+ ->JSON.stringify
24
+
25
+ // A JSON-RPC id is any scalar, and gateways do echo an int id back as a string.
26
+ // Both spellings name the same request, so both are read as the one text.
27
+ // Nullable, not merely optional: JSON-RPC answers a request whose id it could
28
+ // not read with an explicit null.
29
+ let responseIdSchema = S.nullable(
30
+ S.union([S.string, S.int->S.transform(_ => {parser: id => id->Int.toString})]),
31
+ )
32
+
33
+ // The subscribe is the only request this socket ever sends, so an error naming
34
+ // its id is that subscription being refused. So is one naming no id at all —
35
+ // which is how JSON-RPC answers a request it could not parse — but only until
36
+ // the node has confirmed the subscription: after that the request has had its
37
+ // answer, and a null id belongs to whatever else the provider wants to say,
38
+ // throttling most of all. Tearing a delivering subscription down over that
39
+ // would reconnect against an endpoint already asking for less.
40
+ let refusesSubscribe = (id, ~subscribed) =>
41
+ switch id {
42
+ | None => !subscribed
43
+ | Some(id) => id === subscribeRequestIdText
44
+ }
45
+
46
+ let wsMessageSchema = S.union([
47
+ S.object(s => {
48
+ let _ = s.field("method", S.literal("eth_subscription"))
49
+ NewHead(
50
+ s.field(
51
+ "params",
52
+ S.object(s => {
53
+ s.field(
54
+ "result",
55
+ S.object(
56
+ s => {
57
+ s.field("number", Rpc.hexIntSchema)
58
+ },
59
+ ),
60
+ )
61
+ }),
62
+ ),
63
+ )
64
+ }),
65
+ S.object(s => {
66
+ let id = s.field("id", responseIdSchema)
67
+ let _ = s.field("result", S.string)
68
+ Response({id, isError: false})
69
+ }),
70
+ S.object(s => {
71
+ let id = s.field("id", responseIdSchema)
72
+ // The error member as JSON-RPC defines it, rather than `S.unknown`: a
73
+ // missing field parses as `undefined`, which `S.unknown` accepts, and this
74
+ // arm would then match every frame the ones above turned down — including
75
+ // one nobody can read.
76
+ let _ = s.field("error", S.object(s => s.field("code", S.int)))
77
+ Response({id, isError: true})
78
+ }),
79
+ ])
80
+
81
+ let subscribe = (~wsUrl, ~onHeight, ~onStatus) =>
82
+ HeightStream.subscribe(~staleTimeout, ~onHeight, ~onStatus, ~connect=driver => {
83
+ let ws = WebSocket.create(wsUrl)
84
+ let subscribed = ref(false)
85
+
86
+ ws->WebSocket.onopen(() => ws->WebSocket.send(subscribeRequestJson))
87
+
88
+ ws->WebSocket.onmessage(event =>
89
+ if event.data->typeof === #string {
90
+ let text = event.data->(Utils.magic: unknown => string)
91
+ let message = Utils.Option.catchToNone(
92
+ () => text->JSON.parseOrThrow->S.parseOrThrow(wsMessageSchema),
93
+ )
94
+ switch message {
95
+ | Some(NewHead(blockNumber)) => driver.onHeight(blockNumber)
96
+ // An open socket isn't usable until the node accepts the subscription,
97
+ // and only the answer to the subscribe says that it has. A success frame
98
+ // answering anything else would otherwise report a stream as live
99
+ // without one ever having been subscribed.
100
+ | Some(Response({id: Some(id), isError: false})) if id === subscribeRequestIdText =>
101
+ subscribed := true
102
+ driver.onConnected()
103
+ | Some(Response({id, isError: true})) if id->refusesSubscribe(~subscribed=subscribed.contents) =>
104
+ driver.onFailure(~reason=SubscribeRejected, ~detail=text)
105
+ // A provider talking about something other than the subscription this
106
+ // socket asked for — a rate limit, a request nobody here made. Read
107
+ // fine, so not unreadable, and not this stream's to tear itself down
108
+ // over.
109
+ | Some(Response(_)) => ()
110
+ | None => driver.onUnreadable(~detail=text)
111
+ }
112
+ } else {
113
+ // A binary frame from a peer that should only ever be sending JSON text.
114
+ driver.onUnreadable(~detail="binary frame")
115
+ }
116
+ )
117
+
118
+ ws->WebSocket.onerror(error =>
119
+ driver.onFailure(~reason=TransportError, ~detail=?error->JsExn.message)
120
+ )
121
+ ws->WebSocket.onclose(() => driver.onFailure(~reason=Closed))
122
+
123
+ () => ws->WebSocket.close
124
+ })