Skip to content

Conversation

DZakh
Copy link
Member

@DZakh DZakh commented Sep 18, 2025

Summary by CodeRabbit

  • New Features
    • Introduced block-buffered fetching with configurable target buffer size.
    • Added onBlock scheduling support and new head-tracking behavior.
    • Added Array.at utility and Promise.makeAsync binding.
    • New GraphQL type: SimpleEntity.
  • Refactor
    • Reworked APIs to use bufferBlockNumber; removed various currentBlockHeight/legacy flags.
    • Chain/Source managers propagate/calculates targetBufferSize; added runtime metrics.
    • Global state now tracks processedBatches; new StartProcessingBatch action.
  • Chores
    • Removed db_write_timestamp from schemas, templates, and generated tables.
  • Tests
    • Extensive updates: new E2E, rollback suites, buffering/onBlock tests, and mock framework.

Copy link
Contributor

coderabbitai bot commented Sep 18, 2025

Walkthrough

Introduces a buffer-based fetching model across FetchState, ChainFetcher, ChainManager, and GlobalState; removes targetBufferSize from SourceManager.fetchNext’s API; adds Batch.hasUnorderedNextItem; removes db_write_timestamp from schemas/templates; updates templates and tests to align with bufferBlockNumber and new signatures; adds Array.at and Promise.makeAsync bindings.

Changes

Cohort / File(s) Summary
Buffer-based fetching core
codegenerator/cli/npm/envio/src/FetchState.res, codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res, codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res, codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res, codegenerator/cli/npm/envio/src/sources/SourceManager.res, codegenerator/cli/npm/envio/src/sources/SourceManager.resi, codegenerator/cli/npm/envio/src/Batch.res, codegenerator/cli/templates/static/codegen/src/Index.res
Replace head-driven sync with block-buffering (add targetBufferSize, latestOnBlockBlockNumber, bufferBlockNumber). Update getNextQuery/handleQueryResult signatures and flow, propagate targetBufferSize via ChainFetcher/ChainManager, remove it from SourceManager.fetchNext. Add Batch.hasUnorderedNextItem. Global state derives from bufferBlockNumber; processing action updated. Template uses bufferBlockNumber.
DB schema timestamp removal
codegenerator/cli/npm/envio/src/db/InternalTable.res, codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs, codegenerator/cli/npm/envio/src/db/EntityHistory.res, scenarios/test_codegen/test/SerDe_Test.res, scenarios/test_codegen/test/lib_tests/PgStorage_test.res, scenarios/test_codegen/test/entity-column-types-test.ts, scenarios/test_codegen/schema.graphql
Remove db_write_timestamp column and related index from schemas/templates; include field in history dataFields by default handling; update tests and expected SQL/DDL; add SimpleEntity type and enum value.
Utilities and bindings
codegenerator/cli/npm/envio/src/Utils.res, codegenerator/cli/npm/envio/src/bindings/Promise.res
Add Array.at binding; add Promise.makeAsync binding.
Testing scaffolding and new tests
scenarios/test_codegen/test/helpers/Mock.res, scenarios/test_codegen/test/SourceManager_test.res, scenarios/test_codegen/test/lib_tests/FetchState_test.res, scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res, scenarios/test_codegen/test/ChainManager_test.res, scenarios/test_codegen/test/E2E_test.res, scenarios/test_codegen/test/rollback/Rollback_test.res, scenarios/erc20_multichain_factory/test/DynamicContractRecovery_test.res
Introduce Mock.Indexer/Mock.Source harness; refactor tests to new APIs (buffer fields, removed params); add onBlock tests; add E2E start-block test; add comprehensive rollback E2E tests; update chain manager/fetch state tests; pass targetBufferSize where required.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant CM as ChainManager
  participant CF as ChainFetcher
  participant SM as SourceManager
  participant FS as FetchState
  participant Src as Source

  App->>CM: init (compute targetBufferSize)
  CM->>CF: makeFromConfig(..., ~targetBufferSize)
  CF->>FS: make(..., ~targetBufferSize, onBlockConfigs=[])

  loop Fetch cycle
    CM->>SM: fetchNext(~fetchState, ~currentBlockHeight, ~stateId)
    SM->>FS: getNextQuery(fetchState, ~concurrencyLimit, ~currentBlockHeight, ~stateId)
    FS-->>SM: query plan (fromBlock/toBlock/partitions)
    SM->>Src: execute query (getItemsOrThrow)
    Src-->>SM: items, latestFetchedBlock
    SM->>FS: handleQueryResult(~query, ~latestFetchedBlock, ~newItems)
    FS-->>SM: updated buffer/queue (bufferBlockNumber, mutItems)
    SM-->>CM: done
  end

  note over CM,CF: Unordered mode check
  CM->>FS: get state
  CM->>FS: Batch.hasUnorderedNextItem(fetchStates)
  FS-->>CM: bool
  CM-->>App: scheduling decision
Loading
sequenceDiagram
  autonumber
  participant GS as GlobalState
  participant CF as ChainFetcher
  participant FS as FetchState

  GS->>CF: update current block height
  CF->>FS: read bufferBlockNumber
  FS-->>CF: bufferBlockNumber
  CF-->>GS: isFetchingAtHead := bufferBlockNumber >= currentBlockHeight

  GS->>GS: StartProcessingBatch
  GS->>GS: processedBatches += 1 on completion/rollback
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • JonoPrest
  • JasoonS

Poem

I buffered my blocks in a tidy queue,
Hop-hop through heights, as rabbits do.
No timestamp crumbs, just tracks to tread,
Unordered noses sniff ahead.
With promises kept and arrays that chat,
I fetch, I sort—thump!—right where I’m at. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Reproduce duplicate history on rollback bug" accurately describes a clear, central objective in the changeset: new E2E rollback tests and test-harness updates that attempt to reproduce the duplicate-history-on-rollback behavior. While the PR also includes substantial refactors (buffering model, schema/table changes, API signature updates) these do not make the title misleading; it still concisely captures the primary testing objective present in the diff. The title is specific, actionable, and related to real changes in the branch.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dz/rollback-duplicate-history-bug

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DZakh DZakh mentioned this pull request Sep 18, 2025
Base automatically changed from dz/e2e-tests to main September 18, 2025 14:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (22)
scenarios/test_codegen/test/helpers/Mock.res (2)

271-285: Unused gsManagerRef.

Ref is written once and never read; remove to reduce noise.

-    let gsManagerRef = ref(None)
...
-    gsManagerRef := Some(gsManager)

49-69: Expose setEffectCacheOrThrowCalls or remove it.

Counter is created but not part of Storage.t, so it’s inaccessible for assertions.

 type t = {
   isInitializedCalls: array<bool>,
   resolveIsInitialized: bool => unit,
   initializeCalls: array<{
     "entities": array<Internal.entityConfig>,
     "chainConfigs": array<InternalConfig.chain>,
     "enums": array<Internal.enumConfig<Internal.enum>>,
   }>,
   resolveInitialize: Persistence.initialState => unit,
   resumeInitialStateCalls: array<bool>,
   resolveLoadInitialState: Persistence.initialState => unit,
   loadByIdsOrThrowCalls: array<{"ids": array<string>, "tableName": string}>,
   loadByFieldOrThrowCalls: array<{
     "fieldName": string,
     "fieldValue": unknown,
     "tableName": string,
     "operator": Persistence.operator,
   }>,
   dumpEffectCacheCalls: ref<int>,
+  setEffectCacheOrThrowCalls: ref<int>,
   storage: Persistence.storage,
 }

And include it in the returned record where other refs are exposed.

-    let setEffectCacheOrThrowCalls = ref(0)
+    let setEffectCacheOrThrowCalls = ref(0)
...
     {
       isInitializedCalls,
       initializeCalls,
       loadByIdsOrThrowCalls,
       loadByFieldOrThrowCalls,
       dumpEffectCacheCalls,
+      setEffectCacheOrThrowCalls,

Also applies to: 95-96

scenarios/test_codegen/test/E2E_test.res (2)

9-9: Fix typo in test name.

-  Async.it("Currectly starts indexing from a non-zero start block", async () => {
+  Async.it("Correctly starts indexing from a non-zero start block", async () => {

6-7: Remove unused undefined workaround in this file.

It’s not referenced here. Keep it only where needed.

-let undefined = (%raw(`undefined`): option<'a>)
scenarios/erc20_multichain_factory/test/DynamicContractRecovery_test.res (1)

171-172: Hard-coded targetBufferSize; consider centralizing.

If 5000 is a convention, lift it to a shared constant/config to keep tests consistent and avoid drift.

Would you like me to scan tests for other hard-coded buffer sizes and propose a constants module?

codegenerator/cli/templates/static/codegen/src/Index.res (1)

187-187: Clamp latestFetchedBlockNumber to chain startBlock (and endBlock if present).

Using 0 as a floor can mislead for chains with startBlock > 0. Also consider capping to endBlock for TUI correctness once reached.

Apply this diff:

-      let latestFetchedBlockNumber = Pervasives.max(fetchState->FetchState.bufferBlockNumber, 0)
+      let bufferBlock = fetchState->FetchState.bufferBlockNumber
+      let startBlock = cf.chainConfig.startBlock
+      let latestFetchedBlockNumber =
+        switch cf.fetchState.endBlock {
+        | Some(endBlock) => bufferBlock->Belt.Int.max(startBlock)->Belt.Int.min(endBlock)
+        | None => bufferBlock->Belt.Int.max(startBlock)
+        }
codegenerator/cli/npm/envio/src/Batch.res (1)

61-71: Helper looks good; add explicit return type and brief doc.

Small polish for readability and API clarity.

Apply this diff:

-let hasUnorderedNextItem = (fetchStates: ChainMap.t<FetchState.t>) => {
+/** Returns true if any actively indexing chain has an item available (unordered mode). */
+let hasUnorderedNextItem = (fetchStates: ChainMap.t<FetchState.t>): bool => {
   fetchStates
   ->ChainMap.values
   ->Js.Array2.some(fetchState => {
     fetchState->FetchState.isActivelyIndexing &&
       switch fetchState->FetchState.getEarliestEvent {
       | Item(_) => true
       | NoItem(_) => false
       }
   })
 }
scenarios/test_codegen/test/rollback/Rollback_test.res (4)

4-61: Avoid module name shadowing: local Mock hides the imported Mock.

You alias the imported Mock as M, then define a local module Mock. This is confusing and brittle. Prefer renaming the local one (e.g., MockData or LocalMock) to avoid accidental shadowing.

Would you like me to generate a scripted, safe rename across this file?


515-598: Fix assertion message (says “two” but asserts three entities).

The expected list contains 3 SimpleEntity rows; the message claims two.

Apply this diff:

-      ~message="Should have two entities in the db",
+      ~message="Should have three entities in the db",

971-976: Replace FIXME with tracked TODO or adjust expectation when fixing.

Tests contain “// FIXME: This looks wrong” next to history.previous. Either document with an issue link or codify the intended behavior so the test won’t rot post-fix.

Also applies to: 1354-1360


427-441: Extract a microtask “flush” helper to reduce flakiness.

Multiple Utils.delay(0) calls suggest timing sensitivity. Add a shared flush helper (e.g., with Promise.makeAsync twice) and use it across tests.

scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res (1)

96-108: Avoid magic logIndex sentinel; bind to a constant.

16777216 is opaque. If FetchState exposes a base logIndex constant, use it; otherwise define a local const with a comment.

codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res (2)

9-18: Target buffer sizing: confirm the branching logic

The heuristic reduces per-chain buffer (x1) when activeChainsCount > 3, else uses x3. That seems intentional to cap total memory as chains grow. Please confirm this is the intended direction; the AI summary states the opposite.

Optional tidy-up for readability:

-let calculateTargetBufferSize = activeChainsCount => {
-  let targetBatchesInBuffer = 3
-  switch Env.targetBufferSize {
-  | Some(size) => size
-  | None =>
-    Env.maxProcessBatchSize * (
-      activeChainsCount > targetBatchesInBuffer ? 1 : targetBatchesInBuffer
-    )
-  }
-}
+let calculateTargetBufferSize = activeChainsCount => {
+  let minBatches = 1
+  let maxBatches = 3
+  switch Env.targetBufferSize {
+  | Some(size) => size
+  | None => {
+      let batchesPerChain = activeChainsCount > maxBatches ? minBatches : maxBatches
+      Env.maxProcessBatchSize * batchesPerChain
+    }
+  }
+}

48-52: Good metrics surface; consider setting at cold-start too

Prometheus metrics for max batch, target buffer, and reorg threshold are set on resume. Consider also setting IndexingTargetBufferSize in makeFromConfig for cold starts so dashboards don’t miss initial runs.

 let makeFromConfig = (~config: Config.t): t => {
   let targetBufferSize = calculateTargetBufferSize(config.chainMap->ChainMap.size)
+  Prometheus.IndexingTargetBufferSize.set(~targetBufferSize)
   let chainFetchers =
     config.chainMap->ChainMap.map(ChainFetcher.makeFromConfig(_, ~config, ~targetBufferSize))
   {
scenarios/test_codegen/test/ChainManager_test.res (1)

50-55: Off‑by‑one in random event generation loop

for logIndex in 0 to numberOfEventsInBatch generates numberOfEventsInBatch+1 events. If that’s unintentional, iterate up to numberOfEventsInBatch - 1.

-      for logIndex in 0 to numberOfEventsInBatch {
+      if numberOfMockEventsCreated.contents >= 0 { /* keep structure; guard for zero */
+        for logIndex in 0 to (max(numberOfEventsInBatch - 1, -1)) {
           let batchItem = Internal.Event({
             /* ... */
           })
           /* ... */
-      }
+        }
+      }
codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res (2)

153-166: Head-tracking latch may never demote after DC registration

isFetchingAtHead is latched true once bufferBlockNumber >= currentBlockHeight. If dynamic contracts register later (and buffers fall behind), this stays true, potentially overstating “at head” and prematurely setting timestampCaughtUpToHeadOrEndblock.

Consider explicitly demoting on DC registration for that chain if it falls behind:

 let submitPartitionQueryResponse = (
   state,
   ~newItems,
   ~dynamicContracts,
   ~currentBlockHeight,
   ~latestFetchedBlock,
   ~query,
   ~chain,
 ) => {
   let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)

   let updatedChainFetcher =
     chainFetcher
     ->ChainFetcher.handleQueryResult(~query, ~latestFetchedBlock, ~newItems, ~dynamicContracts)
     ->Utils.unwrapResultExn
     ->updateChainFetcherCurrentBlockHeight(~currentBlockHeight)

+  // Demote head status if newly-registered contracts push us behind the head
+  let updatedChainFetcher =
+    dynamicContracts->Js.Array2.length > 0
+      && updatedChainFetcher.isFetchingAtHead
+      && updatedChainFetcher.fetchState->FetchState.bufferBlockNumber < currentBlockHeight
+      ? {...updatedChainFetcher, isFetchingAtHead: false}
+      : updatedChainFetcher

467-472: Add chain context to “All events have been fetched” log

Minor usability: include chain id to ease debugging.

-  if !wasFetchingAtHead && isCurrentlyFetchingAtHead {
-    updatedChainFetcher.logger->Logging.childInfo("All events have been fetched")
-  }
+  if !wasFetchingAtHead && isCurrentlyFetchingAtHead {
+    updatedChainFetcher.logger->Logging.childInfo({
+      "msg": "All events have been fetched",
+      "chainId": updatedChainFetcher.chainConfig.id,
+    })
+  }
scenarios/test_codegen/test/lib_tests/SourceManager_test.res (1)

807-875: Time-based assertions may be flaky under load

Tests rely on Utils.delay with very small intervals (0/1/2 ms). CI noise can cause intermittent failures.

  • Increase pollingInterval/stalledPollingInterval/retry intervals by 1–2 orders of magnitude within tests.
  • Prefer deterministic virtual timers or inject a clock where feasible.

Also applies to: 897-1039, 1041-1179, 1181-1237

codegenerator/cli/npm/envio/src/FetchState.res (4)

254-261: OOM guard: fix off‑by‑one and make pivot robust.

  • Loop can add targetBufferSize+1 items due to <=.
  • maxBlockNumber pivot is taken from potentially unsorted mutItems; this can under‑limit or over‑limit buffering.

Apply:

-      }->Utils.Array.at(-fetchState.targetBufferSize) {
+      }->Utils.Array.at(-fetchState.targetBufferSize) {
         ...
       }
 ...
-      while (
+      while (
         latestOnBlockBlockNumber.contents < maxBlockNumber &&
-          // Additional safeguard to prevent OOM
-          newItemsCounter.contents <= fetchState.targetBufferSize
+          // Additional safeguard to prevent OOM
+          newItemsCounter.contents < fetchState.targetBufferSize
       ) {

Optionally, compute the pivot from the sorted source to avoid relying on unsorted mutItems:

-      let maxBlockNumber = switch switch mutItemsRef.contents {
-      | Some(mutItems) => mutItems
-      | None => fetchState.queue
-      }->Utils.Array.at(-fetchState.targetBufferSize) {
+      let sourceForPivot =
+        switch mutItemsRef.contents {
+        | Some(mutItems) => {
+            let copy = mutItems->Array.copy
+            copy->Js.Array2.sortInPlaceWith(compareBufferItem)
+            copy
+          }
+        | None => fetchState.queue
+        }
+      let maxBlockNumber = switch sourceForPivot->Utils.Array.at(-fetchState.targetBufferSize) {

Also applies to: 268-279, 332-336


331-336: Typos in comments.

  • “asume” → “assume”
  • “respose” → “response”
  • “Comparitor” → “Comparator”

Also applies to: 903-907, 206-208


871-886: Validate targetBufferSize > 0.

Zero or negative values break pivot math (e.g., at(-0) / length - 0) and gating. Enforce at construction.

Apply:

 let make = (
   ~startBlock,
   ~endBlock,
   ~eventConfigs: array<Internal.eventConfig>,
   ~contracts: array<indexingContract>,
   ~maxAddrInPartition,
   ~chainId,
   ~targetBufferSize,
   ~progressBlockNumber=startBlock - 1,
   ~onBlockConfigs=[],
   ~blockLag=0,
 ): t => {
+  if targetBufferSize <= 0 {
+    Js.Exn.raiseError("Invalid configuration: targetBufferSize must be > 0")
+  }

Also applies to: 1007-1011


1287-1291: Guard hasFullBatch against maxBatchSize <= 0.

Avoid unsafe_get and edge-case misclassification.

Apply:

-  let hasFullBatch = ({queue} as fetchState: t, ~maxBatchSize) => {
+  let hasFullBatch = ({queue} as fetchState: t, ~maxBatchSize) => {
+    if maxBatchSize <= 0 { false } else {
       let targetBlockIdx = queue->Array.length - maxBatchSize
       if targetBlockIdx < 0 {
         false
       } else {
         queue->Js.Array2.unsafe_get(targetBlockIdx)->Internal.getItemBlockNumber <=
           fetchState->bufferBlockNumber
       }
-    }
+    }
   }

Also applies to: 1294-1307

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8be862b and 65eacde.

⛔ Files ignored due to path filters (1)
  • scenarios/erc20_multichain_factory/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • codegenerator/cli/npm/envio/src/Batch.res (1 hunks)
  • codegenerator/cli/npm/envio/src/FetchState.res (18 hunks)
  • codegenerator/cli/npm/envio/src/Utils.res (1 hunks)
  • codegenerator/cli/npm/envio/src/bindings/Promise.res (1 hunks)
  • codegenerator/cli/npm/envio/src/db/EntityHistory.res (0 hunks)
  • codegenerator/cli/npm/envio/src/db/InternalTable.res (0 hunks)
  • codegenerator/cli/npm/envio/src/sources/SourceManager.res (0 hunks)
  • codegenerator/cli/npm/envio/src/sources/SourceManager.resi (0 hunks)
  • codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs (0 hunks)
  • codegenerator/cli/templates/static/codegen/src/Index.res (1 hunks)
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res (10 hunks)
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res (5 hunks)
  • codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res (15 hunks)
  • scenarios/erc20_multichain_factory/test/DynamicContractRecovery_test.res (1 hunks)
  • scenarios/test_codegen/schema.graphql (1 hunks)
  • scenarios/test_codegen/test/ChainManager_test.res (3 hunks)
  • scenarios/test_codegen/test/E2E_test.res (1 hunks)
  • scenarios/test_codegen/test/SerDe_Test.res (1 hunks)
  • scenarios/test_codegen/test/entity-column-types-test.ts (1 hunks)
  • scenarios/test_codegen/test/helpers/Mock.res (2 hunks)
  • scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res (1 hunks)
  • scenarios/test_codegen/test/lib_tests/FetchState_test.res (64 hunks)
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res (6 hunks)
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res (17 hunks)
  • scenarios/test_codegen/test/rollback/Rollback_test.res (2 hunks)
💤 Files with no reviewable changes (5)
  • codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs
  • codegenerator/cli/npm/envio/src/sources/SourceManager.res
  • codegenerator/cli/npm/envio/src/db/InternalTable.res
  • codegenerator/cli/npm/envio/src/sources/SourceManager.resi
  • codegenerator/cli/npm/envio/src/db/EntityHistory.res
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{res,resi}

📄 CodeRabbit inference engine (.cursor/rules/rescript.mdc)

**/*.{res,resi}: Never use [| item |] to create an array. Use [ item ] instead.
Must always use = for setting value to a field. Use := only for ref values created using ref function.
ReScript has record types which require a type definition before hand. You can access record fields by dot like foo.myField.
It's also possible to define an inline object, it'll have quoted fields in this case.
Use records when working with structured data, and objects to conveniently pass payload data between functions.
Never use %raw to access object fields if you know the type.

Files:

  • codegenerator/cli/npm/envio/src/bindings/Promise.res
  • codegenerator/cli/npm/envio/src/Batch.res
  • codegenerator/cli/templates/static/codegen/src/Index.res
  • scenarios/test_codegen/test/E2E_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res
  • codegenerator/cli/npm/envio/src/Utils.res
  • scenarios/erc20_multichain_factory/test/DynamicContractRecovery_test.res
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res
  • scenarios/test_codegen/test/rollback/Rollback_test.res
  • codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res
  • scenarios/test_codegen/test/SerDe_Test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_test.res
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res
  • scenarios/test_codegen/test/ChainManager_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
  • codegenerator/cli/npm/envio/src/FetchState.res
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res
  • scenarios/test_codegen/test/helpers/Mock.res
**/*.res

📄 CodeRabbit inference engine (.cursor/rules/navigation.mdc)

Prefer reading ReScript .res modules directly; compiled .js artifacts can be ignored

Files:

  • codegenerator/cli/npm/envio/src/bindings/Promise.res
  • codegenerator/cli/npm/envio/src/Batch.res
  • codegenerator/cli/templates/static/codegen/src/Index.res
  • scenarios/test_codegen/test/E2E_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res
  • codegenerator/cli/npm/envio/src/Utils.res
  • scenarios/erc20_multichain_factory/test/DynamicContractRecovery_test.res
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res
  • scenarios/test_codegen/test/rollback/Rollback_test.res
  • codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res
  • scenarios/test_codegen/test/SerDe_Test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_test.res
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res
  • scenarios/test_codegen/test/ChainManager_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
  • codegenerator/cli/npm/envio/src/FetchState.res
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res
  • scenarios/test_codegen/test/helpers/Mock.res
scenarios/test_codegen/**/*.ts

📄 CodeRabbit inference engine (scenarios/test_codegen/.cursor/rules/hyperindex.mdc)

scenarios/test_codegen/**/*.ts: After changing any TypeScript files, run: pnpm tsc --noEmit to ensure successful compilation
When updating existing entities in handlers, always use the spread operator to create updated objects before persisting
For any external call (e.g., fetch), wrap it in an Effect via experimental_createEffect and consume via context.effect
Use !context.isPreload to skip logic that should not run during preload
In TypeScript, set relationship fields using *_id properties (e.g., token_id) rather than object references
Always cast timestamps from events to BigInt (e.g., BigInt(event.block.timestamp))
When matching addresses in configuration objects within code, ensure keys are lowercase and compare using address.toLowerCase()
Use string | undefined for optional string fields instead of string | null
Always normalize token amounts to a standard decimal (e.g., 18) before addition across tokens; use helpers like normalizeAmountToUSD()

Files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
codegenerator/cli/templates/{static,dynamic}/codegen/src/**/*

📄 CodeRabbit inference engine (.cursor/rules/navigation.mdc)

Edit template versions under codegenerator/cli/templates/static/codegen/src or codegenerator/cli/templates/dynamic/codegen/src instead of editing generated/src

Files:

  • codegenerator/cli/templates/static/codegen/src/Index.res
  • codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res
scenarios/test_codegen/**/schema.graphql

📄 CodeRabbit inference engine (scenarios/test_codegen/.cursor/rules/hyperindex.mdc)

scenarios/test_codegen/**/schema.graphql: After changing schema.graphql, run: pnpm codegen
Do not add the @entity decorator to GraphQL schema types
Avoid time-series aggregate fields (e.g., dailyVolume) in the GraphQL schema
Never use arrays of entities in the schema (e.g., [User!]!)
Model relationships using *_id fields in the schema (e.g., user_id: String!) instead of entity references

Files:

  • scenarios/test_codegen/schema.graphql
🧠 Learnings (14)
📚 Learning: 2025-08-11T08:49:57.399Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/test_codegen/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:49:57.399Z
Learning: Applies to scenarios/test_codegen/**/*.ts : When updating existing entities in handlers, always use the spread operator to create updated objects before persisting

Applied to files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
📚 Learning: 2025-08-11T08:49:57.399Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/test_codegen/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:49:57.399Z
Learning: Applies to scenarios/test_codegen/**/*.ts : In TypeScript, set relationship fields using *_id properties (e.g., token_id) rather than object references

Applied to files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-09-04T14:42:53.500Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: .cursor/rules/subgraph-migration.mdc:0-0
Timestamp: 2025-09-04T14:42:53.500Z
Learning: Applies to src/**/*.ts : Match entity field types exactly with schema: use bigint for BigInt fields, BigDecimal for BigDecimal fields, and correct scalar types

Applied to files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
📚 Learning: 2025-08-11T08:49:57.399Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/test_codegen/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:49:57.399Z
Learning: Applies to scenarios/test_codegen/**/*.ts : Always cast timestamps from events to BigInt (e.g., BigInt(event.block.timestamp))

Applied to files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
📚 Learning: 2025-08-11T08:47:04.346Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/fuel_test/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:47:04.346Z
Learning: Applies to scenarios/fuel_test/**/*.ts : In TypeScript code, set relationship fields using the `_id` form expected by generated types (e.g., `{ token_id: tokenId }`, not `{ token: tokenObject }`)

Applied to files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:47:04.346Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/fuel_test/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:47:04.346Z
Learning: Applies to scenarios/fuel_test/**/*.ts : When updating existing entities in handlers, always use the spread operator to clone immutable returned objects before modifying and saving

Applied to files:

  • scenarios/test_codegen/test/entity-column-types-test.ts
📚 Learning: 2025-08-11T08:47:04.346Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/fuel_test/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:47:04.346Z
Learning: Applies to scenarios/fuel_test/**/*.graphql : Use `entity_id` fields for relationships (e.g., `token_id: string`) rather than direct object references in the GraphQL schema

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:42:57.311Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: codegenerator/cli/templates/static/shared/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:42:57.311Z
Learning: Applies to codegenerator/cli/templates/static/shared/**/*.graphql : In GraphQL schema, use `entity_id` fields (e.g., `token_id: String!`) for relationships instead of direct object references

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:49:57.399Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/test_codegen/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:49:57.399Z
Learning: Applies to scenarios/test_codegen/**/schema.graphql : Model relationships using *_id fields in the schema (e.g., user_id: String!) instead of entity references

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:49:57.399Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/test_codegen/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:49:57.399Z
Learning: Applies to scenarios/test_codegen/**/schema.graphql : Do not add the entity decorator to GraphQL schema types

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:47:04.346Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/fuel_test/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:47:04.346Z
Learning: Applies to scenarios/fuel_test/**/*.graphql : Do not add the `entity` decorator to GraphQL schema types

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:49:57.399Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: scenarios/test_codegen/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:49:57.399Z
Learning: Applies to scenarios/test_codegen/**/schema.graphql : Avoid time-series aggregate fields (e.g., dailyVolume) in the GraphQL schema

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-08-11T08:42:57.311Z
Learnt from: CR
PR: enviodev/hyperindex#0
File: codegenerator/cli/templates/static/shared/.cursor/rules/hyperindex.mdc:0-0
Timestamp: 2025-08-11T08:42:57.311Z
Learning: Applies to codegenerator/cli/templates/static/shared/**/*.graphql : Use `entity_id` fields for relationships instead of entity arrays in the schema

Applied to files:

  • scenarios/test_codegen/schema.graphql
📚 Learning: 2025-05-27T17:07:12.878Z
Learnt from: JonoPrest
PR: enviodev/hyperindex#555
File: scenarios/test_codegen/test/lib_tests/EntityHistory_test.res:631-659
Timestamp: 2025-05-27T17:07:12.878Z
Learning: In isolated tests for EntityHistory rollbacks with controlled mock data setup, the ordering of getRollbackDiff results is deterministic and doesn't require sorting before comparison with deepStrictEqual.

Applied to files:

  • scenarios/test_codegen/test/rollback/Rollback_test.res
🔇 Additional comments (32)
scenarios/test_codegen/schema.graphql (1)

153-156: LGTM: Appropriate addition of SimpleEntity type.

The new SimpleEntity type follows the coding guidelines correctly - it uses basic scalar fields (ID! and String!) without entity decorators or relationship arrays, which aligns with the established patterns for this schema.

scenarios/test_codegen/test/entity-column-types-test.ts (1)

161-161: Correct test expectation update for exact column count.

The change from allowing an extra column (actualColumns.length === expectedColumns.length + 1) to requiring exact match (actualColumns.length === expectedColumns.length) is correct. This aligns with the removal of the db_write_timestamp column from generated entity tables.

scenarios/test_codegen/test/lib_tests/PgStorage_test.res (1)

68-68: Consistent removal of db_write_timestamp from SQL generation tests.

All the test expectations have been correctly updated to remove db_write_timestamp columns from:

  • Entity table CREATE statements (A, B, raw_events)
  • Entity history table structures
  • The ENTITY_TYPE enum now includes SimpleEntity

These changes are consistent with the PR's objective to fix duplicate history on rollback bugs by removing db_write_timestamp infrastructure.

Let me verify the impact of removing db_write_timestamp on entity history and rollback operations:

Also applies to: 78-78, 88-88, 146-146, 150-151, 153-153, 235-235, 317-318

codegenerator/cli/npm/envio/src/Utils.res (1)

306-308: New Array.at binding looks good; verify runtime support.

Binding shape matches other option-returning externals in this file. Please confirm all runtimes (Node/browsers) used by the CLI/tests support Array.prototype.at or are polyfilled.

scenarios/test_codegen/test/rollback/Rollback_test.res (1)

415-418: OK to unblock; remove once on ReScript v12.

The undefined shim is fine for v11. Please gate it with a TODO referencing the upgrade and delete when you bump to v12.

scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res (1)

239-263: Nice coverage of multiple onBlock configs.

Good, deterministic ordering assertions across intervals and indices.

codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res (3)

271-276: Filter cleanup condition looks correct.

Keeping the filter while bufferBlockNumber <= restartBlockNumber and dropping it afterwards matches the comment.

Consider adding a unit test that advances bufferBlockNumber past the threshold to prove cleanup occurs.


469-475: Good: registerDynamicContracts before handling query result.

This ensures indexingContracts are up-to-date for queueing. No currentBlockHeight threading required here.


17-17: isFetchingAtHead field added — call sites migrated

Confirmed: ChainFetcher.t defines isFetchingAtHead and all usages reference the new field or the ChainManager helper; no remaining calls to the old helper were found.

  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res (field + default)
  • codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res (manager helper + Js.Array2.every(cf => cf.isFetchingAtHead))
  • codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res (multiple uses/assignments)
  • scenarios/test_codegen/test/ChainManager_test.res (test fixture)
codegenerator/cli/templates/static/codegen/src/eventFetching/ChainManager.res (3)

21-23: Threading targetBufferSize through fetcher construction looks good

Passing ~targetBufferSize into ChainFetcher.makeFromConfig keeps initialization consistent.


105-108: nextItemIsNone branching is correct

Ordered/unordered branches align with Batch API (getOrderedNextItem vs hasUnorderedNextItem).


213-214: Use of cf.isFetchingAtHead is consistent with new ChainFetcher.t shape

Switching from a helper to the field makes sense given the refactor.

Please confirm all test/mocks initialize isFetchingAtHead (I see updates in ChainManager_test.res).

scenarios/test_codegen/test/ChainManager_test.res (1)

96-117: Mock fetcher shape aligns with ChainFetcher.t

Adding isFetchingAtHead: false keeps tests compatible with the new field.

Also applies to: 114-116

scenarios/test_codegen/test/lib_tests/FetchState_test.res (3)

6-7: Consistent targetBufferSize usage in tests

Threading targetBufferSize via FetchState.make and via state overrides matches the new API.

Also applies to: 80-86, 98-103


126-129: Use of %raw(undefined) is acceptable as a v11 workaround

Acknowledged in comment; safe to keep until bumping ReScript to v12.

Please confirm CI uses v11 where this is needed; remove once on v12.


2824-2851: Buffer overflow prevention test is solid

Clear, deterministic expectations for limiting to maxQueryBlockNumber based on targetBufferSize.

Also applies to: 2893-2915

codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res (4)

171-188: Metadata uses bufferBlockNumber now—confirm semantics

latestFetchedBlockNumber is set from fetchState.bufferBlockNumber. If dashboards previously relied on fully-fetched semantics, confirm this is the intended visible metric.

Also applies to: 183-184


286-317: Synced-at-head timestamp gating looks good; relies on accurate isFetchingAtHead

Given the latch above, correctness depends on proper demotion when new work appears.


659-660: Batch processing state transitions are sound

StartProcessingBatch is dispatched for both progress-only and normal batches; currentlyProcessingBatch is reset on EventBatchProcessed and on invalidation.

Also applies to: 928-941


1080-1093: Reorg filter lifetime condition: double-check boundary

Filter validity uses fetchState.bufferBlockNumber <= firstChangeEvent.blockNumber. Confirm inclusive boundary matches “remove filter once fetched past” intent.

scenarios/test_codegen/test/lib_tests/SourceManager_test.res (2)

1252-1260: executeQuery test: asserts include fromBlock in call records—nice coverage

Good verification of new shape (fromBlock/toBlock/retry).


1319-1420: Retry rotation behavior is well covered

Clear expectation of initial retries, immediate source switching, and rotation cadence.

codegenerator/cli/npm/envio/src/FetchState.res (10)

185-204: Buffer boundary helpers are correct (min frontier).

Semantics of bufferBlockNumber/bufferBlock look good and align with the “ready-to-consume” invariant.


205-216: Comparator matches queue invariant (latest → earliest).

Descending by (blockNumber, logIndex) is consistent with popping from the end.


217-219: Guard blockItemLogIndex against collision with real logIndex.

Ensure runtime logIndex from sources can’t exceed 16,777,216. Add an assert or property test in Internal.item creation, or document the guarantee.


339-351: Metrics updates are sensible.

Partition count, buffer size, and buffer block number are updated atomically with state.


695-701: Queue merge path is correct.

Concatenate then sort at updateInternal keeps the invariant.


979-995: Earliest-item gating vs buffer boundary looks good.

Pop-from-end is consistent with ordering; NoItem returns bufferBlock for consumers.


1129-1137: Initialize latestOnBlockBlockNumber from progress.

Reasonable default. Please add a test covering non-zero progress with onBlockConfigs to ensure no onBlock gaps/duplication across startup.


1164-1171: Rollback sets latestFetchedBlock to previous block.

Conservative choice to re-fetch the block. Confirm this doesn’t reintroduce duplicate onBlock items (given pruning) in mixed Event/Block queues.


1260-1271: Active indexing heuristic OK.

Uses bufferBlockNumber and buffer size to stop after endBlock is passed.


1334-1341: Progress helpers align with buffer boundary.

Correctly derive progress block and next log index only when within buffer.

Also applies to: 1343-1351

Comment on lines 6 to 8
@new
external makeAsync: ((@uncurry 'a => unit, 'e => unit) => promise<unit>) => t<'a> = "Promise"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Don’t use async Promise executors; replace external makeAsync with a safe wrapper.

JS ignores the executor’s returned promise, so async executors won’t propagate rejections to the created Promise, leading to silent failures. Remove the external constructor overload and implement a wrapper that wires rejections correctly.

Apply this diff to remove the unsafe external:

-@new
-external makeAsync: ((@uncurry 'a => unit, 'e => unit) => promise<unit>) => t<'a> = "Promise"

Then add this helper (place it below the existing externals so then/catch are available):

let makeAsync = (executor: (@uncurry 'a => unit, 'e => unit) => promise<unit>): t<'a> =>
  make((resolve, reject) => {
    executor(resolve, reject)
    ->ignoreValue
    ->thenResolve(_ => ())
    ->catch(err => reject(Js.Exn.anyToExnInternal(err)))
    ->done
  })

Run to locate usages that rely on the removed external:


🏁 Script executed:

#!/bin/bash
rg -n --type=res '\bPromise\.makeAsync\b' -C2

Length of output: 27


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
printf "pwd: %s\n" "$(pwd)"

printf "\n--- list .res files (first 200) ---\n"
if command -v fd >/dev/null 2>&1; then
  fd -e res -H | sed -n '1,200p' || true
else
  find . -type f -name '*.res' | sed -n '1,200p' || true
fi

target="codegenerator/cli/npm/envio/src/bindings/Promise.res"
printf "\n--- show %s if present ---\n" "$target"
if [ -f "$target" ]; then
  sed -n '1,200p' "$target"
else
  printf "%s not found\n" "$target"
  if [ -d "codegenerator/cli/npm/envio/src/bindings" ]; then
    printf "\n--- listing directory codegenerator/cli/npm/envio/src/bindings ---\n"
    ls -la codegenerator/cli/npm/envio/src/bindings || true
  fi
fi

printf "\n--- ripgrep searches for makeAsync/usages ---\n"
if command -v rg >/dev/null 2>&1; then
  printf "Searching for 'Promise.makeAsync'...\n"
  rg -n --hidden -S "Promise\\.makeAsync" -g '!*node_modules/**' -C2 || true

  printf "\nSearching for word 'makeAsync'...\n"
  rg -n --hidden -S "\\bmakeAsync\\b" -g '!*node_modules/**' -C2 || true

  printf "\nSearching for 'external makeAsync' definitions...\n"
  rg -n --hidden -S "external\\s+makeAsync" -g '!*node_modules/**' -C2 || true
else
  printf "rg not found, falling back to grep (may be slower)...\n"
  grep -RIn --exclude-dir=node_modules --exclude-dir=vendor --line-number -E "Promise\\.makeAsync|\\bmakeAsync\\b|external\\s+makeAsync" . || true
fi

Length of output: 11484


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
printf "pwd: %s\n" "$(pwd)"

printf "\n--- show Promise.res (with line numbers) ---\n"
nl -ba codegenerator/cli/npm/envio/src/bindings/Promise.res | sed -n '1,240p' || true

printf "\n--- grep for 'makeAsync' or 'Promise.makeAsync' across repo ---\n"
grep -RIn --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist -E '\bmakeAsync\b|Promise\.makeAsync' . || true

Length of output: 540


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf "\n--- Mock.res lines 280-320 ---\n"
sed -n '280,320p' scenarios/test_codegen/test/helpers/Mock.res || true

Length of output: 1499


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf "\n--- scenarios/test_codegen/test/helpers/Mock.res top lines 1-50 ---\n"
sed -n '1,50p' scenarios/test_codegen/test/helpers/Mock.res || true

Length of output: 1339


Replace unsafe async Promise executor with a safe wrapper.

JS ignores the executor’s returned promise, so async executors won’t propagate rejections. Remove the unsafe external and add a wrapper that wires rejections to reject.

Change (file: codegenerator/cli/npm/envio/src/bindings/Promise.res). Usages found: scenarios/test_codegen/test/helpers/Mock.res:295, 304.

Apply this diff to remove the unsafe external:

-@new
-external makeAsync: ((@uncurry 'a => unit, 'e => unit) => promise<unit>) => t<'a> = "Promise"

Then add this helper (place it below the existing externals so then/catch are available):

let makeAsync = (executor: (@uncurry 'a => unit, 'e => unit) => promise<unit>): t<'a> =>
  make((resolve, reject) => {
    executor(resolve, reject)
    ->ignoreValue
    ->thenResolve(_ => ())
    ->catch(err => reject(Js.Exn.anyToExnInternal(err)))
    ->done
  })
🤖 Prompt for AI Agents
In codegenerator/cli/npm/envio/src/bindings/Promise.res around lines 6 to 8,
replace the unsafe external declaration of makeAsync with a safe wrapper: remove
the external binding that maps directly to JS Promise (which ignores any promise
returned by an async executor) and instead add a ReScript helper function
(placed below the other externals so then/catch are available) that calls
make((resolve, reject) => { executor(resolve, reject) |> ignoreValue |>
thenResolve(_ => ()) |> catch(err => reject(Js.Exn.anyToExnInternal(err))) |>
done }), ensuring rejections from an async executor are forwarded to reject;
update usages in scenarios/test_codegen/test/helpers/Mock.res if necessary.

Comment on lines 1236 to 1243
{
...fetchState,
latestOnBlockBlockNumber: firstChangeEvent.blockNumber - 1, // TODO: This is not tested
}->updateInternal(
~partitions,
~indexingContracts,
~queue=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
~mutItems=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
~dcsToStore=switch fetchState.dcsToStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Bug: wrong optional-arg passing to updateInternal (~mutItems).

updateInternal expects option<array<Internal.item>>. Here an array is passed directly, which will not type-check and would skip pruning on rollback if coerced.

Apply:

-  }->updateInternal(
+  }->updateInternal(
     ~partitions,
     ~indexingContracts,
-    ~mutItems=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
+    ~mutItems=?{
+      let pruned = fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent)
+      switch pruned {
+      | [] => None
+      | _ => Some(pruned)
+      }
+    },
     ~dcsToStore=switch fetchState.dcsToStore {

Run to find other call sites that might have the same issue:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 '\bupdateInternal\s*\([^)]*~mutItems\s*='

Length of output: 532


Bug: pass an option for ~mutItems (wrap arrays in Some/None)

updateInternal expects option<array<Internal.item>>; currently an array is passed directly which can skip pruning on rollback.

Apply:

-  }->updateInternal(
+  }->updateInternal(
     ~partitions,
     ~indexingContracts,
-    ~mutItems=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
+    ~mutItems=?{
+      let pruned = fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent)
+      switch pruned {
+      | [] => None
+      | _ => Some(pruned)
+      }
+    },
     ~dcsToStore=switch fetchState.dcsToStore {

Also update test caller: scenarios/test_codegen/test/lib_tests/FetchState_test.res:2633 — replace
~mutItems=[mockEvent(~blockNumber=6)] with ~mutItems=Some([mockEvent(~blockNumber=6)]).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
...fetchState,
latestOnBlockBlockNumber: firstChangeEvent.blockNumber - 1, // TODO: This is not tested
}->updateInternal(
~partitions,
~indexingContracts,
~queue=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
~mutItems=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
~dcsToStore=switch fetchState.dcsToStore {
{
...fetchState,
latestOnBlockBlockNumber: firstChangeEvent.blockNumber - 1, // TODO: This is not tested
}->updateInternal(
~partitions,
~indexingContracts,
~mutItems=?{
let pruned = fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent)
switch pruned {
| [] => None
| _ => Some(pruned)
}
},
~dcsToStore=switch fetchState.dcsToStore {
🤖 Prompt for AI Agents
In codegenerator/cli/npm/envio/src/FetchState.res around lines 1236 to 1243,
update the call to updateInternal so that ~mutItems is passed as an option
(Some(array) or None) instead of passing the array directly; replace
~mutItems=fetchState.queue->pruneQueueFromFirstChangeEvent(~firstChangeEvent)
with wrapping the result in Some(...) or None as appropriate so updateInternal
receives option<array<Internal.item>> and pruning works correctly on rollback.
Also update the test caller in
scenarios/test_codegen/test/lib_tests/FetchState_test.res at the indicated
location (around line 2633) to change ~mutItems=[mockEvent(~blockNumber=6)] to
~mutItems=Some([mockEvent(~blockNumber=6)]).

Comment on lines 295 to 302
Promise.makeAsync(async (resolve, _reject) => {
let before = (gsManager->GlobalStateManager.getState).processedBatches
while before >= (gsManager->GlobalStateManager.getState).processedBatches {
await Utils.delay(50)
}
resolve()
})
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid async Promise executor in getBatchWritePromise.

Use Promise.make with chaining; don’t pass an async executor.

-        Promise.makeAsync(async (resolve, _reject) => {
-          let before = (gsManager->GlobalStateManager.getState).processedBatches
-          while before >= (gsManager->GlobalStateManager.getState).processedBatches {
-            await Utils.delay(50)
-          }
-          resolve()
-        })
+        Promise.make((resolve, _reject) => {
+          let before = (gsManager->GlobalStateManager.getState).processedBatches
+          let rec poll = () => {
+            if before < (gsManager->GlobalStateManager.getState).processedBatches {
+              resolve()
+            } else {
+              Utils.delay(50)->Promise.thenResolve(_ => poll())->Promise.done
+            }
+          }
+          poll()
+        })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Promise.makeAsync(async (resolve, _reject) => {
let before = (gsManager->GlobalStateManager.getState).processedBatches
while before >= (gsManager->GlobalStateManager.getState).processedBatches {
await Utils.delay(50)
}
resolve()
})
},
Promise.make((resolve, _reject) => {
let before = (gsManager->GlobalStateManager.getState).processedBatches
let rec poll = () => {
if before < (gsManager->GlobalStateManager.getState).processedBatches {
resolve()
} else {
Utils.delay(50)->Promise.thenResolve(_ => poll())->Promise.done
}
}
poll()
})
},
🤖 Prompt for AI Agents
In scenarios/test_codegen/test/helpers/Mock.res around lines 295 to 302, the
Promise.makeAsync executor is async (an async function passed to the Promise
constructor) which should be avoided; change this to use Promise.make with
chaining instead of an async executor. Replace the async executor by creating a
Promise.make that kicks off a non-async function which uses a loop implemented
with then-chained calls (or a recursive function) that awaits Utils.delay via
Promise chaining until (gsManager->GlobalStateManager.getState).processedBatches
increments, then resolves; do not use an async executor or await inside the
Promise constructor.

Comment on lines 304 to 316
Promise.makeAsync(async (resolve, _reject) => {
while (
switch (gsManager->GlobalStateManager.getState).rollbackState {
| RollbackInMemStore(_) => false
| RollingBack(_)
| NoRollback => true
}
) {
await Utils.delay(50)
}
resolve()
})
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same issue for getRollbackReadyPromise; replace async executor.

-        Promise.makeAsync(async (resolve, _reject) => {
-          while (
-            switch (gsManager->GlobalStateManager.getState).rollbackState {
-            | RollbackInMemStore(_) => false
-            | RollingBack(_)
-            | NoRollback => true
-            }
-          ) {
-            await Utils.delay(50)
-          }
-          resolve()
-        })
+        Promise.make((resolve, _reject) => {
+          let rec poll = () => {
+            let waiting =
+              switch (gsManager->GlobalStateManager.getState).rollbackState {
+              | RollbackInMemStore(_) => false
+              | RollingBack(_)
+              | NoRollback => true
+              }
+            if waiting {
+              Utils.delay(50)->Promise.thenResolve(_ => poll())->Promise.done
+            } else {
+              resolve()
+            }
+          }
+          poll()
+        })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Promise.makeAsync(async (resolve, _reject) => {
while (
switch (gsManager->GlobalStateManager.getState).rollbackState {
| RollbackInMemStore(_) => false
| RollingBack(_)
| NoRollback => true
}
) {
await Utils.delay(50)
}
resolve()
})
},
Promise.make((resolve, _reject) => {
let rec poll = () => {
let waiting =
switch (gsManager->GlobalStateManager.getState).rollbackState {
| RollbackInMemStore(_) => false
| RollingBack(_)
| NoRollback => true
}
if waiting {
Utils.delay(50)->Promise.thenResolve(_ => poll())->Promise.done
} else {
resolve()
}
}
poll()
})
},
🤖 Prompt for AI Agents
In scenarios/test_codegen/test/helpers/Mock.res around lines 304 to 316,
getRollbackReadyPromise uses an async function as the Promise.makeAsync
executor; change the executor to a non-async function that invokes an async IIFE
so the executor itself is synchronous. Concretely, replace the async (resolve,
_reject) => { ... await ...; resolve() } with (resolve, reject) => { (async ()
=> { ... await ...; resolve() })().catch(reject) } so awaiting and error
handling happen inside the IIFE and the Promise executor is not async.

Comment on lines 474 to 476
resolve({
Source.currentBlockHeight,
reorgGuard: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: using module name instead of param (currentBlockHeight).

You’re returning Source.currentBlockHeight, which is undefined here. Use the function parameter.

-                    Source.currentBlockHeight,
+                    currentBlockHeight,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resolve({
Source.currentBlockHeight,
reorgGuard: {
resolve({
currentBlockHeight,
reorgGuard: {
🤖 Prompt for AI Agents
In scenarios/test_codegen/test/helpers/Mock.res around lines 474 to 476, the
resolve call returns Source.currentBlockHeight (a module name) instead of the
function parameter currentBlockHeight; replace Source.currentBlockHeight with
the local parameter currentBlockHeight so the resolved object uses the actual
value passed into the function.

Assert.equal(
createQuery,
`CREATE TABLE IF NOT EXISTS "public"."EntityWithAllNonArrayTypes"("bigDecimal" NUMERIC NOT NULL, "bigDecimalWithConfig" NUMERIC(10, 8) NOT NULL, "bigInt" NUMERIC NOT NULL, "bool" BOOLEAN NOT NULL, "enumField" "public".AccountType NOT NULL, "float_" DOUBLE PRECISION NOT NULL, "id" TEXT NOT NULL, "int_" INTEGER NOT NULL, "optBigDecimal" NUMERIC, "optBigInt" NUMERIC, "optBool" BOOLEAN, "optEnumField" "public".AccountType, "optFloat" DOUBLE PRECISION, "optInt" INTEGER, "optString" TEXT, "string" TEXT NOT NULL, "db_write_timestamp" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY("id"));`,
`CREATE TABLE IF NOT EXISTS "public"."EntityWithAllNonArrayTypes"("bigDecimal" NUMERIC NOT NULL, "bigDecimalWithConfig" NUMERIC(10, 8) NOT NULL, "bigInt" NUMERIC NOT NULL, "bool" BOOLEAN NOT NULL, "enumField" "public".AccountType NOT NULL, "float_" DOUBLE PRECISION NOT NULL, "id" TEXT NOT NULL, "int_" INTEGER NOT NULL, "optBigDecimal" NUMERIC, "optBigInt" NUMERIC, "optBool" BOOLEAN, "optEnumField" "public".AccountType, "optFloat" DOUBLE PRECISION, "optInt" INTEGER, "optString" TEXT, "string" TEXT NOT NULL, PRIMARY KEY("id"));`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify removal of db_write_timestamp aligns with the PR's rollback bug fix.

The test correctly removes the db_write_timestamp column from the expected CREATE TABLE statement for EntityWithAllNonArrayTypes. This change aligns with the broader removal of db_write_timestamp columns across the codebase as part of fixing the "duplicate history on rollback bug" mentioned in the PR title.

However, please verify that removing db_write_timestamp doesn't break entity history tracking or rollback functionality:


🏁 Script executed:

#!/bin/bash
# Verify that entity history functionality still works without db_write_timestamp
rg -n "db_write_timestamp" --type=res -C3 -g '!**/test/**'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Re-run search for db_write_timestamp references outside of tests (no file-type filter)
rg -n "db_write_timestamp" -C3 -g '!**/test/**'

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "PWD: $(pwd)"
git rev-parse --abbrev-ref HEAD 2>/dev/null || true
echo "---- rg version ----"
rg --version 2>/dev/null || echo "rg not found"

echo "---- Search (rg) across repo including hidden/unignored files ----"
rg -n --hidden -uu "db_write_timestamp" || true

echo "---- Search results excluding common test paths ----"
rg -n --hidden -uu "db_write_timestamp" 2>/dev/null | grep -v -E '/test/|/tests/|/spec/' || echo "no matches outside test paths"

echo "---- Fallback: git grep across tracked files ----"
git grep -n "db_write_timestamp" || true
git grep -n "db_write_timestamp" | grep -v -E '/test/|/tests/|/spec/' || echo "git grep: no matches outside test paths"

echo "---- Repo root listing (first 200 entries) ----"
find . -maxdepth 3 -type f | sed -n '1,200p'

Length of output: 3552


Remove remaining db_write_timestamp references & verify rollback/history

  • Found occurrences that still reference db_write_timestamp: codegenerator/cli/src/config_parsing/entity_parsing.rs:355 and codegenerator/integration_tests/tests/evm_Erc20.js:13 — remove or update these to match the CREATE TABLE change.
  • After updating, run integration/e2e tests and explicitly verify entity history and rollback behavior end-to-end.
🤖 Prompt for AI Agents
In scenarios/test_codegen/test/SerDe_Test.res around line 129, and in the two
files called out by the review
(codegenerator/cli/src/config_parsing/entity_parsing.rs line ~355 and
codegenerator/integration_tests/tests/evm_Erc20.js line ~13), there are leftover
references to db_write_timestamp that no longer exist in the CREATE TABLE
(EntityWithAllNonArrayTypes) schema; remove or update those references to use
the current schema fields (or the new timestamp column name if renamed), adjust
any parsing/serialization logic to stop expecting db_write_timestamp, and update
tests to assert the new behavior; after making the changes run the full
integration/e2e test suite and explicitly verify entity history and rollback
behavior end-to-end to ensure no lingering dependencies on db_write_timestamp
remain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant