envio 3.6.1 → 3.7.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 (80) hide show
  1. package/index.d.ts +197 -9
  2. package/package.json +6 -6
  3. package/src/Batch.res +64 -25
  4. package/src/Batch.res.mjs +69 -46
  5. package/src/ChainFetching.res +13 -17
  6. package/src/ChainFetching.res.mjs +9 -14
  7. package/src/ChainState.res +160 -119
  8. package/src/ChainState.res.mjs +75 -46
  9. package/src/ChainState.resi +15 -12
  10. package/src/Config.res +9 -2
  11. package/src/Config.res.mjs +4 -4
  12. package/src/Core.res +5 -0
  13. package/src/Ecosystem.res +0 -1
  14. package/src/Envio.res +16 -3
  15. package/src/EventConfigBuilder.res +155 -40
  16. package/src/EventConfigBuilder.res.mjs +76 -24
  17. package/src/EventProcessing.res +4 -8
  18. package/src/EventProcessing.res.mjs +4 -4
  19. package/src/EventUtils.res +3 -47
  20. package/src/FetchState.res +124 -104
  21. package/src/FetchState.res.mjs +116 -100
  22. package/src/HandlerRegister.res +108 -2
  23. package/src/HandlerRegister.res.mjs +59 -10
  24. package/src/HandlerRegister.resi +4 -0
  25. package/src/Internal.res +84 -24
  26. package/src/Internal.res.mjs +33 -2
  27. package/src/LazyLoader.res +12 -11
  28. package/src/LazyLoader.res.mjs +13 -7
  29. package/src/Main.res +22 -9
  30. package/src/Main.res.mjs +18 -5
  31. package/src/MemoryStorage.res +724 -0
  32. package/src/MemoryStorage.res.mjs +601 -0
  33. package/src/PgStorage.res +18 -7
  34. package/src/PgStorage.res.mjs +14 -11
  35. package/src/ReorgDetection.res +0 -222
  36. package/src/ReorgDetection.res.mjs +0 -163
  37. package/src/Rollback.res +14 -15
  38. package/src/Rollback.res.mjs +4 -5
  39. package/src/SimulateItems.res +2 -2
  40. package/src/Utils.res +3 -0
  41. package/src/bindings/ClickHouse.res +16 -11
  42. package/src/bindings/ClickHouse.res.mjs +10 -10
  43. package/src/bindings/Vitest.res +1 -1
  44. package/src/sources/BlockStore.res +115 -5
  45. package/src/sources/BlockStore.res.mjs +25 -0
  46. package/src/sources/Evm.res +0 -1
  47. package/src/sources/Evm.res.mjs +0 -1
  48. package/src/sources/EvmHyperSyncSource.res +19 -84
  49. package/src/sources/EvmHyperSyncSource.res.mjs +24 -60
  50. package/src/sources/Fuel.res +24 -4
  51. package/src/sources/Fuel.res.mjs +23 -3
  52. package/src/sources/FuelHyperSync.res +8 -3
  53. package/src/sources/FuelHyperSync.res.mjs +5 -4
  54. package/src/sources/FuelHyperSync.resi +3 -1
  55. package/src/sources/FuelHyperSyncClient.res +7 -10
  56. package/src/sources/FuelHyperSyncSource.res +18 -45
  57. package/src/sources/FuelHyperSyncSource.res.mjs +18 -35
  58. package/src/sources/HyperSync.res +49 -229
  59. package/src/sources/HyperSync.res.mjs +74 -191
  60. package/src/sources/HyperSync.resi +11 -35
  61. package/src/sources/HyperSyncClient.res +9 -79
  62. package/src/sources/HyperSyncClient.res.mjs +2 -6
  63. package/src/sources/RequestStat.res +5 -0
  64. package/src/sources/RequestStat.res.mjs +2 -0
  65. package/src/sources/RpcSource.res +149 -43
  66. package/src/sources/RpcSource.res.mjs +104 -41
  67. package/src/sources/SimulateSource.res +8 -5
  68. package/src/sources/SimulateSource.res.mjs +6 -6
  69. package/src/sources/Source.res +89 -16
  70. package/src/sources/Source.res.mjs +50 -1
  71. package/src/sources/SourceManager.res +255 -50
  72. package/src/sources/SourceManager.res.mjs +149 -55
  73. package/src/sources/SourceManager.resi +2 -6
  74. package/src/sources/Svm.res +0 -7
  75. package/src/sources/Svm.res.mjs +0 -8
  76. package/src/sources/SvmHyperSyncClient.res +21 -16
  77. package/src/sources/SvmHyperSyncClient.res.mjs +3 -4
  78. package/src/sources/SvmHyperSyncSource.res +25 -117
  79. package/src/sources/SvmHyperSyncSource.res.mjs +8 -121
  80. package/svm.schema.json +3 -2
@@ -14,8 +14,13 @@ type execParams = {query: string}
14
14
  @module("@clickhouse/client")
15
15
  external createClient: clientConfig => client = "createClient"
16
16
 
17
+ // `command`, not `exec`: exec hands its response stream to the caller and holds
18
+ // the socket until it is consumed, which nothing here does. The pool is 10
19
+ // sockets wide, so a schema whose DDL needs more than that — initialize issues
20
+ // 2N+3 statements for N entities — stalled every statement past the tenth until
21
+ // the 30s request timeout freed one. command destroys the stream for us.
17
22
  @send
18
- external exec: (client, execParams) => promise<unit> = "exec"
23
+ external command: (client, execParams) => promise<unit> = "command"
19
24
 
20
25
  @send
21
26
  external close: client => promise<unit> = "close"
@@ -628,21 +633,21 @@ let initialize = async (
628
633
  // CLUSTER removes the database from every node — the engine's own log
629
634
  // can't replicate the drop of the database it lives in — and SYNC waits
630
635
  // for the drop to finish before the CREATE below.
631
- await client->exec({
636
+ await client->command({
632
637
  query: `DROP DATABASE IF EXISTS ${database} ON CLUSTER '{cluster}' SYNC`,
633
638
  })
634
639
  } else {
635
- await client->exec({
640
+ await client->command({
636
641
  query: `TRUNCATE DATABASE IF EXISTS ${database}${onClusterClause(~onCluster=ddlOnCluster)}`,
637
642
  })
638
643
  }
639
- await client->exec({
644
+ await client->command({
640
645
  query: `CREATE DATABASE IF NOT EXISTS ${database}${databaseOnClusterClause}${databaseEngineClause}`,
641
646
  })
642
647
 
643
648
  await Promise.all(
644
649
  entities->Array.map(entityConfig =>
645
- client->exec({
650
+ client->command({
646
651
  query: makeCreateHistoryTableQuery(
647
652
  ~entityConfig,
648
653
  ~database,
@@ -653,7 +658,7 @@ let initialize = async (
653
658
  })
654
659
  ),
655
660
  )->Utils.Promise.ignoreValue
656
- await client->exec({
661
+ await client->command({
657
662
  query: makeCreateCheckpointsTableQuery(
658
663
  ~database,
659
664
  ~replicated,
@@ -670,14 +675,14 @@ let initialize = async (
670
675
  // caught up before creating the views. ON CLUSTER must precede the
671
676
  // database name in this command's grammar.
672
677
  if hasReplicatedDatabaseEngine {
673
- await client->exec({
678
+ await client->command({
674
679
  query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ${database}`,
675
680
  })
676
681
  }
677
682
 
678
683
  await Promise.all(
679
684
  entities->Array.map(entityConfig =>
680
- client->exec({
685
+ client->command({
681
686
  query: makeCreateViewQuery(~entityConfig, ~database, ~onCluster=ddlOnCluster),
682
687
  })
683
688
  ),
@@ -697,7 +702,7 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
697
702
  try {
698
703
  // Try to use the database - will throw if it doesn't exist
699
704
  try {
700
- await client->exec({query: `USE ${database}`})
705
+ await client->command({query: `USE ${database}`})
701
706
  } catch {
702
707
  | exn =>
703
708
  Logging.errorWithExn(
@@ -717,14 +722,14 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
717
722
  await Promise.all(
718
723
  tables->Array.map(table => {
719
724
  let tableName = table["name"]
720
- client->exec({
725
+ client->command({
721
726
  query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->BigInt.toString}`,
722
727
  })
723
728
  }),
724
729
  )->Utils.Promise.ignoreValue
725
730
 
726
731
  // Delete stale checkpoints
727
- await client->exec({
732
+ await client->command({
728
733
  query: `DELETE FROM ${database}.\`${InternalTable.Checkpoints.table.tableName}\` WHERE \`${Table.idFieldName}\` > ${checkpointId->BigInt.toString}`,
729
734
  })
730
735
  } catch {
@@ -428,31 +428,31 @@ async function initialize(client, database, entities, param, chainIdModeOpt) {
428
428
  }
429
429
  }
430
430
  if (hasReplicatedDatabaseEngine) {
431
- await client.exec({
431
+ await client.command({
432
432
  query: `DROP DATABASE IF EXISTS ` + database + ` ON CLUSTER '{cluster}' SYNC`
433
433
  });
434
434
  } else {
435
- await client.exec({
435
+ await client.command({
436
436
  query: `TRUNCATE DATABASE IF EXISTS ` + database + (
437
437
  ddlOnCluster ? ` ON CLUSTER '{cluster}'` : ""
438
438
  )
439
439
  });
440
440
  }
441
- await client.exec({
441
+ await client.command({
442
442
  query: `CREATE DATABASE IF NOT EXISTS ` + database + databaseOnClusterClause + databaseEngineClause
443
443
  });
444
- await Promise.all(entities.map(entityConfig => client.exec({
444
+ await Promise.all(entities.map(entityConfig => client.command({
445
445
  query: makeCreateHistoryTableQuery(entityConfig, database, replicated, ddlOnCluster, chainIdMode)
446
446
  })));
447
- await client.exec({
447
+ await client.command({
448
448
  query: makeCreateCheckpointsTableQuery(database, replicated, ddlOnCluster, chainIdMode)
449
449
  });
450
450
  if (hasReplicatedDatabaseEngine) {
451
- await client.exec({
451
+ await client.command({
452
452
  query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ` + database
453
453
  });
454
454
  }
455
- await Promise.all(entities.map(entityConfig => client.exec({
455
+ await Promise.all(entities.map(entityConfig => client.command({
456
456
  query: makeCreateViewQuery(entityConfig, database, ddlOnCluster)
457
457
  })));
458
458
  return Logging.trace("ClickHouse storage initialization completed successfully");
@@ -466,7 +466,7 @@ async function initialize(client, database, entities, param, chainIdModeOpt) {
466
466
  async function resume(client, database, checkpointId) {
467
467
  try {
468
468
  try {
469
- await client.exec({
469
+ await client.command({
470
470
  query: `USE ` + database
471
471
  });
472
472
  } catch (raw_exn) {
@@ -480,11 +480,11 @@ async function resume(client, database, checkpointId) {
480
480
  let tables = (await tablesResult.json()).data;
481
481
  await Promise.all(tables.map(table => {
482
482
  let tableName = table.name;
483
- return client.exec({
483
+ return client.command({
484
484
  query: `ALTER TABLE ` + database + `.\`` + tableName + `\` DELETE WHERE \`` + EntityHistory.checkpointIdFieldName + `\` > ` + checkpointId.toString()
485
485
  });
486
486
  }));
487
- return await client.exec({
487
+ return await client.command({
488
488
  query: `DELETE FROM ` + database + `.\`` + InternalTable.Checkpoints.table.tableName + `\` WHERE \`` + Table.idFieldName + `\` > ` + checkpointId.toString()
489
489
  });
490
490
  } catch (raw_exn$1) {
@@ -98,7 +98,7 @@ external afterEach: (unit => unit) => unit = "afterEach"
98
98
  // Async Module
99
99
  // ============================================================================
100
100
 
101
- type options = {retry?: int}
101
+ type options = {retry?: int, timeout?: int}
102
102
 
103
103
  module Async = {
104
104
  @module("vitest")
@@ -4,6 +4,12 @@
4
4
  // page that is merged in. At batch preparation the selected fields are
5
5
  // materialised in bulk, off the JS thread, in columnar form and zipped into
6
6
  // plain JS objects on the main thread.
7
+ //
8
+ // The store also owns response validation and reorg detection. A response page
9
+ // records conflicts found while it is built; SourceManager rejects those pages
10
+ // before the persistent store is touched. Merging a validated page compares
11
+ // only persistent-vs-response hashes, and pruning keeps the hash of processed
12
+ // blocks still inside the reorg threshold.
7
13
  type t
8
14
 
9
15
  @send external newEvm: (Core.blockStoreCtor, ~shouldChecksum: bool) => t = "newEvm"
@@ -26,8 +32,93 @@ let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool): t => {
26
32
  // with the Rust store, `EvmBlockField`).
27
33
  let makeMaskFn = FieldMask.makeMaskFn
28
34
 
29
- // Drain another store (a fetch-response page) into this one.
30
- @send external merge: (t, t) => unit = "merge"
35
+ // Sparse JS blocks accepted by the `fromJs*` page constructors. Every field is
36
+ // optional except the key, so a page can carry anything from a full block to a
37
+ // hash-only reorg observation. The Rust side re-encodes them through the same
38
+ // column fill as fetched blocks.
39
+ type evmBlockInput = {number: int, hash?: string, timestamp?: int}
40
+ type svmBlockInput = {slot: int, hash?: string, time?: int}
41
+ type fuelBlockInput = {height: int, id?: string, time?: int}
42
+
43
+ @send
44
+ external fromJsEvm: (Core.blockStoreCtor, array<evmBlockInput>, bool) => t = "fromJsEvm"
45
+ @send
46
+ external fromJsSvm: (Core.blockStoreCtor, array<svmBlockInput>) => t = "fromJsSvm"
47
+ @send
48
+ external fromJsFuel: (Core.blockStoreCtor, array<fuelBlockInput>) => t = "fromJsFuel"
49
+
50
+ // An ecosystem-agnostic (number, hash, timestamp) observation, mapped onto the
51
+ // ecosystem's own field names when the page is built.
52
+ type inputBlock = {blockNumber: int, blockHash?: string, blockTimestamp?: int}
53
+
54
+ // Build a page from JS-observed blocks (RPC responses, stored reorg
55
+ // checkpoints) for merging into the per-chain store.
56
+ let fromJs = (blocks: array<inputBlock>, ~ecosystem: Ecosystem.name, ~shouldChecksum): t => {
57
+ let ctor = Core.getAddon().blockStore
58
+ switch ecosystem {
59
+ | Evm =>
60
+ ctor->fromJsEvm(
61
+ blocks->Array.map(b => {
62
+ number: b.blockNumber,
63
+ hash: ?b.blockHash,
64
+ timestamp: ?b.blockTimestamp,
65
+ }),
66
+ shouldChecksum,
67
+ )
68
+ | Svm =>
69
+ ctor->fromJsSvm(
70
+ blocks->Array.map(b => {
71
+ slot: b.blockNumber,
72
+ hash: ?b.blockHash,
73
+ time: ?b.blockTimestamp,
74
+ }),
75
+ )
76
+ | Fuel =>
77
+ ctor->fromJsFuel(
78
+ blocks->Array.map(b => {
79
+ height: b.blockNumber,
80
+ id: ?b.blockHash,
81
+ time: ?b.blockTimestamp,
82
+ }),
83
+ )
84
+ }
85
+ }
86
+
87
+ // The lowest merged block at or above the reorg threshold whose received hash
88
+ // differed from the stored one.
89
+ type hashMismatch = {
90
+ blockNumber: int,
91
+ storedHash: string,
92
+ receivedHash: string,
93
+ }
94
+
95
+ // Drain another store (a fetch-response page) into this one, comparing hashes
96
+ // on the way. Blocks below `fromBlock` (outside the reorg threshold) or without
97
+ // a hash on either side are merged without comparison. On a mismatch the page
98
+ // is discarded - the stored hashes stay for the rollback comparison - unless
99
+ // `reportOnly` is set (detect-only mode), which merges anyway so the same
100
+ // mismatch doesn't re-report on every response.
101
+ @send
102
+ external merge: (t, t, ~fromBlock: int, ~reportOnly: bool) => Null.t<hashMismatch> = "merge"
103
+
104
+ // Append a backend page to a logical response store. This always appends rows;
105
+ // any conflict is retained as response metadata for SourceManager to validate.
106
+ @send
107
+ external appendPage: (t, t) => unit = "appendPage"
108
+
109
+ // A conflict observed within the response itself. Such a response is
110
+ // discarded and retried, rather than treated as a chain reorg.
111
+ @send external responseConflict: t => Null.t<hashMismatch> = "responseConflict"
112
+
113
+ // Requested block numbers not covered by this response. SVM gaps count as
114
+ // covered when HyperSync's cursor has fully processed their half-open range;
115
+ // parent slot/hash links are validated separately as response consistency.
116
+ @send external missingHashes: (t, array<int>) => array<int> = "missingHashes"
117
+
118
+ // Compare a validated response store against the persistent store in ascending
119
+ // block order and stop at the first mismatch.
120
+ @send
121
+ external latestValidBlockFromStore: (t, t, array<int>) => Null.t<int> = "latestValidBlockFromStore"
31
122
 
32
123
  // Bulk-materialise blocks off the JS thread, one row per `blockNumbers[i]` key,
33
124
  // decoding only the fields set in that row's own `masks[i]`. Result is aligned
@@ -39,8 +130,27 @@ external materialize: (
39
130
  ~masks: array<float>,
40
131
  ) => promise<array<Internal.eventBlock>> = "materialize"
41
132
 
42
- // Drop blocks at or below the given block (already processed).
43
- @send external prune: (t, int) => unit = "prune"
133
+ // Drop blocks at or below the given block (already processed), keeping the
134
+ // hashes of blocks at or above `keepHashesFrom` for reorg detection.
135
+ @send external prune: (t, int, ~keepHashesFrom: int) => unit = "prune"
44
136
 
45
- // Drop blocks above the given block (rolled back).
137
+ // Drop blocks above the given block (rolled back), hashes included. The
138
+ // rolled-back range is refetched, so keeping any of its hashes would compare
139
+ // the refetch against a fork the rollback already disproved.
46
140
  @send external rollback: (t, int) => unit = "rollback"
141
+
142
+ // Hash of a stored block, if the store still holds it.
143
+ @send external getHash: (t, int) => Null.t<string> = "getHash"
144
+
145
+ // Block numbers in `[fromBlock, belowBlock)` with a stored hash, ascending.
146
+ @send
147
+ external getHashedBlockNumbers: (t, ~fromBlock: int, ~belowBlock: int) => array<int> =
148
+ "getHashedBlockNumbers"
149
+
150
+ // Every stored hash in `[fromBlock, belowBlock)` as two aligned columns,
151
+ // ascending by block number - one crossing for what `getHash` would charge per
152
+ // block.
153
+ type hashedBlocks = {blockNumbers: array<int>, hashes: array<string>}
154
+
155
+ @send
156
+ external getHashes: (t, ~fromBlock: int, ~belowBlock: int) => hashedBlocks = "getHashes"
@@ -15,10 +15,35 @@ function make(ecosystem, shouldChecksum) {
15
15
  }
16
16
  }
17
17
 
18
+ function fromJs(blocks, ecosystem, shouldChecksum) {
19
+ let ctor = Core.getAddon().BlockStore;
20
+ switch (ecosystem) {
21
+ case "evm" :
22
+ return ctor.fromJsEvm(blocks.map(b => ({
23
+ number: b.blockNumber,
24
+ hash: b.blockHash,
25
+ timestamp: b.blockTimestamp
26
+ })), shouldChecksum);
27
+ case "fuel" :
28
+ return ctor.fromJsFuel(blocks.map(b => ({
29
+ height: b.blockNumber,
30
+ id: b.blockHash,
31
+ time: b.blockTimestamp
32
+ })));
33
+ case "svm" :
34
+ return ctor.fromJsSvm(blocks.map(b => ({
35
+ slot: b.blockNumber,
36
+ hash: b.blockHash,
37
+ time: b.blockTimestamp
38
+ })));
39
+ }
40
+ }
41
+
18
42
  let makeMaskFn = FieldMask.makeMaskFn;
19
43
 
20
44
  export {
21
45
  make,
22
46
  makeMaskFn,
47
+ fromJs,
23
48
  }
24
49
  /* Core Not a pure module */
@@ -76,7 +76,6 @@ let make = (~logger: Pino.t): Ecosystem.t => {
76
76
  blockNumberName: "number",
77
77
  blockTimestampName: "timestamp",
78
78
  blockHashName: "hash",
79
- cleanUpRawEventFieldsInPlace,
80
79
  onBlockMethodName: "onBlock",
81
80
  // EVM filter shape: `{block: {number: {_gte?, _lte?, _every?}}}`.
82
81
  // The inner range chunk is returned as raw `S.unknown` and parsed a
@@ -55,7 +55,6 @@ function make(logger) {
55
55
  blockNumberName: "number",
56
56
  blockTimestampName: "timestamp",
57
57
  blockHashName: "hash",
58
- cleanUpRawEventFieldsInPlace: cleanUpRawEventFieldsInPlace,
59
58
  onBlockMethodName: "onBlock",
60
59
  onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
61
60
  onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
@@ -79,7 +79,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
79
79
  payload: {
80
80
  contractName: onEventRegistration.eventConfig.contractName,
81
81
  eventName: onEventRegistration.eventConfig.name,
82
- chainId: chainId,
82
+ chainId,
83
83
  params: item.params,
84
84
  srcAddress,
85
85
  logIndex,
@@ -112,33 +112,23 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
112
112
  ~addressSet,
113
113
  ~clientFilteredContracts=selection.clientFilteredContracts,
114
114
  ) catch {
115
- | HyperSync.GetLogs.Error(error) =>
115
+ | HyperSync.GetLogs.Error(WrongInstance) =>
116
+ throw(Source.SourceBehindHead({blockNumber: fromBlock, requestStats: []}))
117
+ | HyperSync.GetLogs.Error(UnexpectedMissingParams({missingParams})) =>
116
118
  throw(
117
119
  Source.GetItemsError(
118
120
  Source.FailedGettingItems({
119
121
  exn: %raw(`null`),
120
122
  attemptedToBlock: toBlock->Option.getOr(knownHeight),
121
- retry: switch error {
122
- | WrongInstance =>
123
- let backoffMillis = switch retry {
124
- | 0 => 100
125
- | _ => 500 * retry
126
- }
127
- WithBackoff({
128
- message: `Block #${fromBlock->Int.toString} not found in HyperSync. HyperSync has multiple instances and it's possible that they drift independently slightly from the head. Indexing should continue correctly after retrying the query in ${backoffMillis->Int.toString}ms.`,
129
- backoffMillis,
130
- })
131
- | UnexpectedMissingParams({missingParams}) =>
132
- ImpossibleForTheQuery({
133
- message: `Source returned invalid data with missing required fields: ${missingParams->Array.joinUnsafe(
134
- ", ",
135
- )}`,
136
- })
137
- },
123
+ retry: ImpossibleForTheQuery({
124
+ message: `Source returned invalid data with missing required fields: ${missingParams->Array.joinUnsafe(
125
+ ", ",
126
+ )}`,
127
+ }),
138
128
  }),
139
129
  ),
140
130
  )
141
- | Source.RateLimited(_) as exn => throw(exn)
131
+ | (Source.RateLimited(_) | Source.SourceBehindHead(_)) as exn => throw(exn)
142
132
  | exn =>
143
133
  throw(
144
134
  Source.GetItemsError(
@@ -173,13 +163,6 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
173
163
  //Parse page items into queue items
174
164
  let parsedQueueItems = []
175
165
 
176
- // Block headers are returned once per number; items reference them by blockNumber.
177
- let blocksByNumber = Utils.Map.make()
178
- pageUnsafe.blocks->Array.forEach(block => {
179
- blocksByNumber->Utils.Map.set(block.number, block)->ignore
180
- })
181
- let getBlock = blockNumber => blocksByNumber->Utils.Map.unsafeGet(blockNumber)
182
-
183
166
  pageUnsafe.items->Array.forEach(item => {
184
167
  let onEventRegistration = onEventRegistrations->Array.getUnsafe(item.onEventRegistrationIndex)
185
168
  parsedQueueItems
@@ -189,43 +172,6 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
189
172
 
190
173
  let parsingTimeElapsed = parsingTimeRef->Performance.secondsSince
191
174
 
192
- // Collect (blockNumber, blockHash) pairs we already have from the response —
193
- // one per returned block plus, when present, the rollbackGuard's head block
194
- // and the parent of the range's first block. Duplicates are allowed; reorg
195
- // detection notices same-block-number-different-hash collisions itself.
196
- let blockHashes = []
197
- pageUnsafe.blocks->Array.forEach(block => {
198
- blockHashes
199
- ->Array.push({ReorgDetection.blockNumber: block.number, blockHash: block.hash})
200
- ->ignore
201
- })
202
- switch pageUnsafe.rollbackGuard {
203
- | None => ()
204
- | Some({blockNumber, hash, firstBlockNumber, firstParentHash}) => {
205
- blockHashes->Array.push({ReorgDetection.blockNumber, blockHash: hash})->ignore
206
- blockHashes
207
- ->Array.push({
208
- ReorgDetection.blockNumber: firstBlockNumber - 1,
209
- blockHash: firstParentHash,
210
- })
211
- ->ignore
212
- }
213
- }
214
-
215
- // Best-effort timestamp for the queried-range head: prefer the rollbackGuard
216
- // (set at the head for unconfirmed blocks), otherwise the last item if it
217
- // happens to be in the range's last block. 0 is a tolerated placeholder
218
- // when neither is available (FetchState already uses 0 in several spots).
219
- let latestFetchedBlockTimestamp = switch pageUnsafe.rollbackGuard {
220
- | Some({timestamp}) => timestamp
221
- | None =>
222
- switch pageUnsafe.items->Array.get(pageUnsafe.items->Array.length - 1) {
223
- | Some(item) if item.blockNumber == heighestBlockQueried =>
224
- getBlock(item.blockNumber).timestamp
225
- | _ => 0
226
- }
227
- }
228
-
229
175
  let totalTimeElapsed = totalTimeRef->Performance.secondsSince
230
176
 
231
177
  let stats = {
@@ -235,30 +181,24 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
235
181
  }
236
182
 
237
183
  {
238
- latestFetchedBlockTimestamp,
239
184
  parsedQueueItems,
240
185
  transactionStore: Some(pageUnsafe.transactionStore),
241
- blockStore: Some(pageUnsafe.blockStore),
186
+ // The page store also carries the rollbackGuard's blocks (head block and
187
+ // parent of the range's first block), inserted on the Rust side.
188
+ blockStore: pageUnsafe.blockStore,
242
189
  latestFetchedBlockNumber: heighestBlockQueried,
243
190
  stats,
244
191
  knownHeight,
245
- blockHashes,
246
192
  fromBlockQueried: fromBlock,
247
193
  requestStats,
248
194
  }
249
195
  }
250
196
 
251
- let getBlockHashes = (~blockNumbers, ~logger) =>
252
- HyperSync.queryBlockDataMulti(
253
- ~client,
254
- ~blockNumbers,
255
- ~sourceName=name,
256
- ~chainId=chainId,
257
- ~logger,
258
- )->Promise.thenResolve(((queryRes, requestStats)) => {
259
- Source.result: queryRes->HyperSync.mapExn,
260
- requestStats,
261
- })
197
+ // Called through the client rather than passed as a value: the client is a
198
+ // napi class, so a detached method reference loses the instance it belongs to.
199
+ let getBlockHashes = HyperSync.makeGetBlockHashes(
200
+ ~query=(~blockNumbers) => client.getBlockHashes(~blockNumbers),
201
+ )
262
202
 
263
203
  {
264
204
  name,
@@ -287,11 +227,6 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
287
227
  },
288
228
  getItemsOrThrow,
289
229
  createHeightSubscription: (~onHeight) =>
290
- HyperSyncHeightStream.subscribe(
291
- ~hyperSyncUrl=endpointUrl,
292
- ~apiToken,
293
- ~chainId=chainId,
294
- ~onHeight,
295
- ),
230
+ HyperSyncHeightStream.subscribe(~hyperSyncUrl=endpointUrl, ~apiToken, ~chainId, ~onHeight),
296
231
  }
297
232
  }
@@ -22,7 +22,6 @@ function make(param) {
22
22
  let onEventRegistrations = param.onEventRegistrations;
23
23
  let endpointUrl = param.endpointUrl;
24
24
  let chainId = param.chainId;
25
- let name = "HyperSync";
26
25
  let apiToken$1 = apiToken !== undefined ? apiToken : Stdlib_JsError.throwWithMessage(`An Envio API token is required for using HyperSync as a data-source.
27
26
  Set the ENVIO_API_TOKEN environment variable in your .env file.
28
27
  Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
@@ -58,22 +57,16 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
58
57
  let pageUnsafe;
59
58
  try {
60
59
  pageUnsafe = await HyperSync.GetLogs.query(client, fromBlock, toBlock, itemsTarget, selection.onEventRegistrations.map(reg => reg.index), addressSet, selection.clientFilteredContracts);
61
- } catch (raw_error) {
62
- let error = Primitive_exceptions.internalToException(raw_error);
63
- if (error.RE_EXN_ID === HyperSync.GetLogs.$$Error) {
64
- let error$1 = error._1;
65
- let tmp;
66
- if (typeof error$1 !== "object") {
67
- let backoffMillis = retry !== 0 ? 500 * retry | 0 : 100;
68
- tmp = {
69
- TAG: "WithBackoff",
70
- message: `Block #` + fromBlock.toString() + ` not found in HyperSync. HyperSync has multiple instances and it's possible that they drift independently slightly from the head. Indexing should continue correctly after retrying the query in ` + backoffMillis.toString() + `ms.`,
71
- backoffMillis: backoffMillis
72
- };
73
- } else {
74
- tmp = {
75
- TAG: "ImpossibleForTheQuery",
76
- message: `Source returned invalid data with missing required fields: ` + error$1.missingParams.join(", ")
60
+ } catch (raw_exn) {
61
+ let exn = Primitive_exceptions.internalToException(raw_exn);
62
+ if (exn.RE_EXN_ID === HyperSync.GetLogs.$$Error) {
63
+ let match = exn._1;
64
+ if (typeof match !== "object") {
65
+ throw {
66
+ RE_EXN_ID: Source.SourceBehindHead,
67
+ blockNumber: fromBlock,
68
+ requestStats: [],
69
+ Error: new Error()
77
70
  };
78
71
  }
79
72
  throw {
@@ -82,19 +75,25 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
82
75
  TAG: "FailedGettingItems",
83
76
  exn: null,
84
77
  attemptedToBlock: Stdlib_Option.getOr(toBlock, knownHeight),
85
- retry: tmp
78
+ retry: {
79
+ TAG: "ImpossibleForTheQuery",
80
+ message: `Source returned invalid data with missing required fields: ` + match.missingParams.join(", ")
81
+ }
86
82
  },
87
83
  Error: new Error()
88
84
  };
89
85
  }
90
- if (error.RE_EXN_ID === Source.RateLimited) {
91
- throw error;
86
+ if (exn.RE_EXN_ID === Source.RateLimited) {
87
+ throw exn;
88
+ }
89
+ if (exn.RE_EXN_ID === Source.SourceBehindHead) {
90
+ throw exn;
92
91
  }
93
92
  throw {
94
93
  RE_EXN_ID: Source.GetItemsError,
95
94
  _1: {
96
95
  TAG: "FailedGettingItems",
97
- exn: error,
96
+ exn: exn,
98
97
  attemptedToBlock: Stdlib_Option.getOr(toBlock, knownHeight),
99
98
  retry: {
100
99
  TAG: "WithBackoff",
@@ -114,41 +113,11 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
114
113
  let heighestBlockQueried = pageUnsafe.nextBlock - 1 | 0;
115
114
  let parsingTimeRef = Performance.now();
116
115
  let parsedQueueItems = [];
117
- let blocksByNumber = new Map();
118
- pageUnsafe.blocks.forEach(block => {
119
- blocksByNumber.set(block.number, block);
120
- });
121
116
  pageUnsafe.items.forEach(item => {
122
117
  let onEventRegistration = onEventRegistrations[item.onEventRegistrationIndex];
123
118
  parsedQueueItems.push(makeEventBatchQueueItem(item, onEventRegistration));
124
119
  });
125
120
  let parsingTimeElapsed = Performance.secondsSince(parsingTimeRef);
126
- let blockHashes = [];
127
- pageUnsafe.blocks.forEach(block => {
128
- blockHashes.push({
129
- blockHash: block.hash,
130
- blockNumber: block.number
131
- });
132
- });
133
- let match = pageUnsafe.rollbackGuard;
134
- if (match !== undefined) {
135
- blockHashes.push({
136
- blockHash: match.hash,
137
- blockNumber: match.blockNumber
138
- });
139
- blockHashes.push({
140
- blockHash: match.firstParentHash,
141
- blockNumber: match.firstBlockNumber - 1 | 0
142
- });
143
- }
144
- let match$1 = pageUnsafe.rollbackGuard;
145
- let latestFetchedBlockTimestamp;
146
- if (match$1 !== undefined) {
147
- latestFetchedBlockTimestamp = match$1.timestamp;
148
- } else {
149
- let item = pageUnsafe.items[pageUnsafe.items.length - 1 | 0];
150
- latestFetchedBlockTimestamp = item !== undefined && item.blockNumber === heighestBlockQueried ? blocksByNumber.get(item.blockNumber).timestamp : 0;
151
- }
152
121
  let totalTimeElapsed = Performance.secondsSince(totalTimeRef);
153
122
  let stats_parsing$unknowntime$unknown$lpars$rpar = parsingTimeElapsed;
154
123
  let stats_page$unknownfetch$unknowntime$unknown$lpars$rpar = pageFetchTime;
@@ -159,23 +128,18 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
159
128
  };
160
129
  return {
161
130
  knownHeight: knownHeight$1,
162
- blockHashes: blockHashes,
163
131
  parsedQueueItems: parsedQueueItems,
164
132
  transactionStore: Primitive_option.some(pageUnsafe.transactionStore),
165
- blockStore: Primitive_option.some(pageUnsafe.blockStore),
133
+ blockStore: pageUnsafe.blockStore,
166
134
  fromBlockQueried: fromBlock,
167
135
  latestFetchedBlockNumber: heighestBlockQueried,
168
- latestFetchedBlockTimestamp: latestFetchedBlockTimestamp,
169
136
  stats: stats,
170
137
  requestStats: requestStats
171
138
  };
172
139
  };
173
- let getBlockHashes = (blockNumbers, logger) => HyperSync.queryBlockDataMulti(client, blockNumbers, name, chainId, logger).then(param => ({
174
- result: HyperSync.mapExn(param[0]),
175
- requestStats: param[1]
176
- }));
140
+ let getBlockHashes = HyperSync.makeGetBlockHashes(blockNumbers => client.getBlockHashes(blockNumbers));
177
141
  return {
178
- name: name,
142
+ name: "HyperSync",
179
143
  sourceFor: "Sync",
180
144
  chainId: chainId,
181
145
  poweredByHyperSync: true,
@@ -232,4 +196,4 @@ export {
232
196
  isUnauthorizedError,
233
197
  make,
234
198
  }
235
- /* Logging Not a pure module */
199
+ /* Source Not a pure module */