Skip to content

Conversation

@Gmin2
Copy link
Contributor

@Gmin2 Gmin2 commented Sep 4, 2025

Description

Implemented dynamic datasource removing themseleves and adding them at end blocks

Fixes #2099

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist

  • I have tested locally
  • I have performed a self review of my changes
  • Updated any relevant documentation
  • Linked to any relevant issues
  • I have added tests relevant to my changes
  • Any dependent changes have been merged and published in downstream modules
  • My code is up to date with the base branch
  • I have updated relevant changelogs. We suggest using chan

Summary by CodeRabbit

  • New Features

    • Destroy dynamic datasources by template+index; query active datasources by template; lookup datasource params by index.
    • Optional endBlock on datasources to mark lifecycle end; host/worker/global bindings expose destroy/get operations.
  • Behavior

    • Indexing now tracks block height and excludes datasources with endBlock ≤ current height; filtering updates after destroy.
  • Tests

    • Expanded tests for destruction flows, endBlock propagation, template-scoped queries, error cases, and in-memory state validation.

✏️ Tip: You can customize this high-level summary in your review settings.

Copy link
Collaborator

@stwiname stwiname left a comment

Choose a reason for hiding this comment

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

This is on the right track but it is flawed currently.

With dynamic datasources there can be multiple instances from the same template, the only differences being the start block and arguments. So there needs to be more information passed through to remove the correct datasource.

I think the best solution for this is to provide the ability to list dynamic datasources for a template. Then the user can select the appropriate one to remove by providing the index.

@Gmin2 Gmin2 requested a review from stwiname October 17, 2025 06:36
@Gmin2
Copy link
Contributor Author

Gmin2 commented Oct 17, 2025

@stwiname just implemented listing of dynamic datasources and to remove it by index no

async destroyDynamicDatasource(
templateName: string,
currentBlockHeight: number,
index?: number,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Index should be required, it then simplifies this code and avoids users accidentally removing all dynamic datasources.

await this.dynamicDsService.destroyDynamicDatasource(templateName, blockHeight, index);

// Mark datasources with this template for removal from current processing
filteredDataSources.forEach((fds) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The destroyed dynamic datasources should be removed from filteredDatasources, at this point it shouldn't be used anymore.

…ces should be removed from filteredDatasources
@Gmin2 Gmin2 force-pushed the dynamic-datasource branch from c580346 to 4971c3d Compare October 22, 2025 16:02
.filter(({params}) => params.templateName === templateName && params.endBlock === undefined);

return matchingDatasources.map(({globalIndex, params}, index) => ({
index,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be the global index?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes thnx for this !

Comment on lines 143 to 145
const globalIndex = this._datasourceParams.findIndex(
(p) => p.templateName === dsInfo.templateName && p.startBlock === dsInfo.startBlock && p.endBlock === undefined
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just have the index on DynamicDatasourceInfo then there's no need to filter by templateName, find the index, then find the global index.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nice one, implemented in af7339f

const dsParam = (this.dynamicDsService as any)._datasourceParams?.find(
(p: any) =>
p.templateName === (fds as any).mapping?.file?.split('/').pop()?.replace('.js', '') ||
(p.startBlock === (fds as any).startBlock && p.templateName === templateName)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is sufficient, there could be multiple instances of a dynamic datasource with the same startBlock but different args.

Comment on lines 141 to 142
filteredDataSources.length = 0;
filteredDataSources.push(...updatedFilteredDataSources);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just filteredDatasources = updatedFilteredDataSources?

@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

Walkthrough

Adds endBlock tracking and runtime destructor/getter APIs for dynamic datasources: new types and global VM bindings; DynamicDsService gains endBlock-aware creation, querying and destruction; worker host functions and indexer manager propagate blockHeight and re-filter datasources after destruction; tests expanded for endBlock and destructor behavior.

Changes

Cohort / File(s) Change Summary
Type Definitions
packages/types-core/src/interfaces.ts
Added DynamicDatasourceInfo interface and exported types DynamicDatasourceDestructor and DynamicDatasourceGetter.
Global API Exports
packages/types-core/src/global.ts
Exposed global VM bindings destroyDynamicDatasource and getDynamicDatasources.
Core Dynamic DS Service
packages/node-core/src/indexer/dynamic-ds.service.ts
Added endBlock?: number to DatasourceParams; added destroyDynamicDatasource, getDynamicDatasourcesByTemplate, getDatasourceParamByIndex; updated getTemplate/getDatasource to accept/propagate endBlock; destruction updates in-memory params/_datasources, sets runtime endBlock, persists metadata, and validates template/index.
Tests (dynamic DS)
packages/node-core/src/indexer/dynamic-ds.service.spec.ts
Test helper getTemplate signature updated to accept endBlock; project.templates extended with Other; added testParamOther; many tests added for destruction, error cases, creation after destruction, endBlock propagation, multi-template/index behaviors, and in-memory state assertions.
Indexer VM Sandbox
packages/node-core/src/indexer/indexer.manager.ts
Bound VM functions to dynamic DS service: getDynamicDatasources(templateName) and destroyDynamicDatasource(templateName, blockHeight, index); destroyDynamicDatasource re-filters datasources after destruction; active filtering uses > comparison to nextProcessingHeight.
Worker Dynamic DS Support
packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts
Extended HostDynamicDS and hostDynamicDsKeys with dynamicDsDestroyDynamicDatasource, dynamicDsGetDynamicDatasourcesByTemplate, dynamicDsGetDatasourceParamByIndex; added WorkerDynamicDsService delegations and host-function bindings.
Node Indexer Flow
packages/node/src/indexer/indexer.manager.ts
Threaded blockHeight into indexContent and call sites; switched datasource iteration to indexed for loop to allow early removal when a datasource is destroyed; skip datasources with endBlock defined and <= current blockHeight.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Handler as Handler
    participant VM as VM Sandbox
    participant Manager as Indexer Manager
    participant Service as DynamicDsService
    participant Store as Metadata Store

    Handler->>VM: destroyDynamicDatasource(templateName, index)
    VM->>Manager: destroyDynamicDatasource(templateName, blockHeight, index)
    Manager->>Service: destroyDynamicDatasource(templateName, blockHeight, index)
    Service->>Service: getDatasourceParamByIndex(index)
    alt datasource found & active
        Service->>Service: set param.endBlock = blockHeight
        Service->>Service: update in-memory _datasourceParams/_datasources
        Service->>Store: persist updated params (metadata)
        Service-->>Manager: success
        Manager->>Manager: re-filter filteredDataSources (apply endBlock)
        Manager-->>VM: ack
        VM-->>Handler: continue (datasource excluded for future handlers)
    else not found / already destroyed
        Service-->>Manager: throw error
        Manager-->>VM: propagate error
        VM-->>Handler: error returned
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect synchronization between updates to _datasourceParams and _datasources and the persistence step.
  • Validate bounds/template-name checks and error paths in destroyDynamicDatasource.
  • Verify VM/worker host binding names, argument passing (blockHeight) and cross-thread delegation.
  • Confirm the indexed for-loop in indexer.manager handles removals without skipping subsequent items.
  • Review new tests for brittle ordering/index assumptions.

Poem

🐰 I hop through code and mark the end,

A template closed by my small friend.
End blocks set, indices tidy, neat,
Metadata saved — the lifecycle complete.
Hooray, the dynamic datasources retreat!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive Code changes implement the core requirements from #2099: dynamic datasources can be destroyed via destroyDynamicDatasource method [#2099], endBlock support is added to DatasourceParams, getDynamicDatasourcesByTemplate provides listing capability [#2099], and test coverage is expanded substantially. However, documentation updates, codegen modifications, and example projects are not reflected in the code changes. Verify that linked issue #2099 requirements for documentation, codegen updates, and example projects are being tracked separately or will be addressed in follow-up work.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature: dynamic datasources can now remove themselves and be added with end blocks, which aligns with the primary changes across all modified files.
Out of Scope Changes check ✅ Passed All changes directly support the dynamic datasource destruction feature: DatasourceParams.endBlock, destroyDynamicDatasource, getDynamicDatasourcesByTemplate, endBlock handling in template creation, worker integration, and global interface exports align with #2099 objectives.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

@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: 0

🧹 Nitpick comments (2)
packages/node-core/src/indexer/indexer.manager.ts (1)

140-149: Consider a more robust comparison for args.

The current implementation uses JSON.stringify to compare datasource arguments (lines 144-145), which can produce false negatives if object keys are in different orders. Additionally, the fallback chain for retrieving args from processor.options || options is fragile and may not correctly locate args across different datasource types.

Consider using a deep-equality utility (e.g., lodash's isEqual) or normalizing the comparison by sorting object keys before stringification.

Apply this approach:

+import {isEqual} from 'lodash';

  // Filter out the destroyed datasource by matching startBlock and args
  // Note: Reassigning filteredDataSources is intentional - subsequent handlers
  // within the same block will see the updated filtered list
  filteredDataSources = filteredDataSources.filter((fds) => {
    const fdsStartBlock = (fds as any).startBlock;
    // For custom datasources, args are stored in processor.options
    // For runtime datasources, they may be stored differently
-   const fdsArgs = JSON.stringify((fds as any).processor?.options || (fds as any).options || {});
-   const paramArgs = JSON.stringify(destroyedDsParam.args || {});
+   const fdsArgs = (fds as any).processor?.options || (fds as any).options || {};
+   const paramArgs = destroyedDsParam.args || {};

    // Keep datasource if it doesn't match the destroyed one
-   return !(fdsStartBlock === destroyedDsParam.startBlock && fdsArgs === paramArgs);
+   return !(fdsStartBlock === destroyedDsParam.startBlock && isEqual(fdsArgs, paramArgs));
  });
packages/node-core/src/indexer/dynamic-ds.service.ts (1)

137-183: Consider type-safe endBlock assignment.

The destruction logic is comprehensive with good validation. However, line 178 uses as any to assign endBlock to the datasource object, which bypasses TypeScript's type safety.

If the datasource type doesn't include endBlock, consider:

  1. Updating the base datasource types to include an optional endBlock property, or
  2. Documenting why the type assertion is necessary (e.g., if datasource types are dynamically extended at runtime)

This would improve type safety and make the code's intent clearer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acc14d4 and cd60d99.

📒 Files selected for processing (6)
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6 hunks)
  • packages/node-core/src/indexer/dynamic-ds.service.ts (4 hunks)
  • packages/node-core/src/indexer/indexer.manager.ts (2 hunks)
  • packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2 hunks)
  • packages/types-core/src/global.ts (2 hunks)
  • packages/types-core/src/interfaces.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/types-core/src/global.ts (3)
packages/node-core/src/indexer/dynamic-ds.service.ts (2)
  • destroyDynamicDatasource (137-183)
  • getDynamicDatasources (186-196)
packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2)
  • destroyDynamicDatasource (40-42)
  • getDynamicDatasources (44-46)
packages/types-core/src/interfaces.ts (2)
  • DynamicDatasourceDestructor (5-5)
  • DynamicDatasourceGetter (26-26)
packages/node-core/src/indexer/indexer.manager.ts (1)
packages/node-core/logger.js (1)
  • logger (5-5)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2)
packages/node-core/src/indexer/dynamic-ds.service.ts (2)
  • DatasourceParams (18-23)
  • IDynamicDsService (25-32)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
🔇 Additional comments (9)
packages/node-core/src/indexer/indexer.manager.ts (2)

92-92: LGTM: Correct mutability change.

Changing from const to let is necessary to support the runtime filtering of destroyed datasources at line 140.


119-122: LGTM: Clean injection of getDynamicDatasources.

The function properly delegates to the dynamic datasource service and follows the established pattern for VM injections.

packages/types-core/src/global.ts (1)

5-5: LGTM: Clean addition of global bindings.

The new imports and global declarations properly expose the dynamic datasource destruction and querying APIs, aligning with the interfaces defined in interfaces.ts.

Also applies to: 15-16

packages/types-core/src/interfaces.ts (1)

5-26: LGTM: Well-designed public API types.

The new type definitions are clear, well-documented, and provide a clean public API surface for dynamic datasource lifecycle management. The DynamicDatasourceInfo interface appropriately includes the global index, making destruction straightforward.

packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (1)

6-21: LGTM: Proper worker-host bridge implementation.

The new methods correctly extend the worker-host communication bridge for dynamic datasource destruction and template-based querying, maintaining consistency with the existing delegation pattern.

Also applies to: 40-50, 56-59

packages/node-core/src/indexer/dynamic-ds.service.ts (3)

98-122: LGTM: Clean template-based datasource querying.

The implementation correctly filters datasources by template name and excludes destroyed ones. Using the global index in the returned DynamicDatasourceInfo objects is the right approach for enabling destruction by index.


124-135: LGTM: Simple and safe bounds-checked getter.

The implementation provides straightforward access to datasource parameters by global index with appropriate bounds checking.


213-219: LGTM: Template construction properly includes endBlock.

The signature update and implementation correctly handle the optional endBlock parameter, ensuring it's properly propagated to constructed datasources.

packages/node-core/src/indexer/dynamic-ds.service.spec.ts (1)

18-20: LGTM: Comprehensive test coverage for new functionality.

The test suite thoroughly covers:

  • Happy path: creation, destruction, and querying
  • Error cases: bounds checking, already destroyed, template mismatches
  • Edge cases: global indexing after multiple destructions, multiple templates, reset behavior
  • Integration: metadata persistence and in-memory consistency

This meets the PR objective of achieving at least 80% test coverage on new code.

Also applies to: 27-27, 44-44, 74-438

@Gmin2 Gmin2 requested a review from stwiname October 23, 2025 11:03
Comment on lines 131 to 134
if (!this._datasourceParams || index < 0 || index >= this._datasourceParams.length) {
return undefined;
}
return this._datasourceParams[index];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (!this._datasourceParams || index < 0 || index >= this._datasourceParams.length) {
return undefined;
}
return this._datasourceParams[index];
return this._datasourceParams?.[index];

Comment on lines 147 to 151
// Validate the global index is within bounds
if (index < 0 || index >= this._datasourceParams.length) {
throw new Error(
`Index ${index} is out of bounds. There are ${this._datasourceParams.length} datasource(s) in total`
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this can be removed and instead check that dsParam below is not undefined.

Comment on lines 142 to 145
// For custom datasources, args are stored in processor.options
// For runtime datasources, they may be stored differently
const fdsArgs = JSON.stringify((fds as any).processor?.options || (fds as any).options || {});
const paramArgs = JSON.stringify(destroyedDsParam.args || {});
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think a better option might be to call filterDataSources again on the filtered datasources.

// Note: Reassigning filteredDataSources is intentional - subsequent handlers
// within the same block will see the updated filtered list
filteredDataSources = filteredDataSources.filter((fds) => {
const fdsStartBlock = (fds as any).startBlock;
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be good to remove this any cast

@Gmin2 Gmin2 requested a review from stwiname October 28, 2025 09:45
Copy link

@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: 1

🧹 Nitpick comments (3)
packages/node-core/src/indexer/indexer.manager.ts (1)

119-123: Optional: return immutable copies from getDynamicDatasources.

Handlers could accidentally mutate returned info objects. Consider returning Object.freeze()-d copies to harden the VM API. No behavior change required.

packages/node-core/src/indexer/dynamic-ds.service.spec.ts (1)

262-333: Add tests for identical startBlock with different args + param lookup.

  • Add a case with two 'Test' datasources sharing startBlock but different args; destroy by global index should affect only the targeted one.
  • Add a small unit for getDatasourceParamByIndex (valid index, OOB).

Example:

it('destroys by global index when startBlock collides but args differ', async () => {
  const a = {templateName:'Test', startBlock: 10, args:{id: 'A'}};
  const b = {templateName:'Test', startBlock: 10, args:{id: 'B'}};
  const meta = mockMetadata([a, b]);
  await service.init(meta);
  await service.destroyDynamicDatasource('Test', 20, 1); // destroy 'B'
  const params = (service as any)._datasourceParams;
  expect(params[0]).toEqual(a);
  expect(params[1]).toEqual({...b, endBlock: 20});
});

it('getDatasourceParamByIndex returns undefined when OOB', async () => {
  const meta = mockMetadata([testParam1]);
  await service.init(meta);
  expect(service.getDatasourceParamByIndex(5)).toBeUndefined();
  expect(service.getDatasourceParamByIndex(0)).toEqual(testParam1);
});
packages/node-core/src/indexer/dynamic-ds.service.ts (1)

134-181: Add a guard to prevent destruction before startBlock.

Defensive check avoids setting endBlock earlier than the datasource starts.

Apply this diff:

   // Validate it matches the template name and is not already destroyed
   if (dsParam.templateName !== templateName) {
     throw new Error(
       `Datasource at index ${index} has template name "${dsParam.templateName}", not "${templateName}"`
     );
   }

   if (dsParam.endBlock !== undefined) {
     throw new Error(`Dynamic datasource at index ${index} is already destroyed`);
   }
+
+  // Prevent destroying before the datasource has started
+  if (currentBlockHeight < dsParam.startBlock) {
+    throw new Error(
+      `Cannot destroy datasource at index ${index} before its startBlock (${dsParam.startBlock})`
+    );
+  }

Optionally, expose a helper for the manager to remove items in-place without array scans:

+  /** Returns the DS object by global index (active or destroyed). */
+  getDatasourceByIndex(index: number): DS | undefined {
+    return this._datasources?.[index];
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd60d99 and 2905c79.

📒 Files selected for processing (3)
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6 hunks)
  • packages/node-core/src/indexer/dynamic-ds.service.ts (4 hunks)
  • packages/node-core/src/indexer/indexer.manager.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
🔇 Additional comments (6)
packages/node-core/src/indexer/indexer.manager.ts (1)

92-92: LGTM on making filteredDataSources mutable.

Using let here is required for subsequent in-block updates.

packages/node-core/src/indexer/dynamic-ds.service.spec.ts (2)

18-20: LGTM: exposing getTemplate for tests.

Keeps production visibility intact while enabling endBlock assertions.


74-87: Destroy lifecycle tests look solid.

Good coverage of success paths, OOB, double-destroy, cross-template, and metadata sync.

Also applies to: 194-217, 334-437

packages/node-core/src/indexer/dynamic-ds.service.ts (3)

5-5: LGTM: types import and endBlock in params.

endBlock on DatasourceParams and DynamicDatasourceInfo usage align with the new lifecycle.

Also applies to: 22-22


98-122: Getter by template with global indices looks good.

Clear contract: returns active items and exposes global index for destruction.


210-217: LGTM: endBlock propagation into templates and ds objects.

Ensures destroyed state is reflected in memory and persisted via updateDynamicDs.

Also applies to: 219-221

Comment on lines 124 to 144
// Inject function to destroy ds into vm
vm.freeze(async (templateName: string, index: number) => {
await this.dynamicDsService.destroyDynamicDatasource(templateName, blockHeight, index);

// Re-filter datasources to exclude the destroyed one
// The destroyed datasource now has endBlock set, so filterDataSources will exclude it
// Note: Reassigning filteredDataSources is intentional - subsequent handlers
// within the same block will see the updated filtered list
filteredDataSources = this.filterDataSources(blockHeight, filteredDataSources);
}, 'destroyDynamicDatasource');

Copy link

@coderabbitai coderabbitai bot Oct 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

In-block destroy isn’t effective: reassign doesn’t affect current iteration; filter logic won’t exclude endBlock === current block.

  • Reassigning filteredDataSources won’t change the array that indexBlockData is iterating.
  • filterDataSources includes ds where endBlock >= height, so a destroyed ds with endBlock === blockHeight will still be included for this block.

Fix by removing the destroyed DS from filteredDataSources in place using object identity.

Apply this diff:

         vm.freeze(async (templateName: string, index: number) => {
           await this.dynamicDsService.destroyDynamicDatasource(templateName, blockHeight, index);

-          // Re-filter datasources to exclude the destroyed one
-          // The destroyed datasource now has endBlock set, so filterDataSources will exclude it
-          // Note: Reassigning filteredDataSources is intentional - subsequent handlers
-          // within the same block will see the updated filtered list
-          filteredDataSources = this.filterDataSources(blockHeight, filteredDataSources);
+          // Remove the destroyed DS from the current list IN PLACE so the rest of this block
+          // no longer schedules it. EndBlock handles exclusion from subsequent blocks.
+          const allDynamic = await this.dynamicDsService.getDynamicDatasources();
+          const destroyed = allDynamic[index] as DS | undefined;
+          if (destroyed) {
+            const pos = filteredDataSources.indexOf(destroyed);
+            if (pos !== -1) filteredDataSources.splice(pos, 1);
+          }
         }, 'destroyDynamicDatasource');

Run this script to confirm indexBlockData iterates the original array (so reassigning the variable is ineffective):


🏁 Script executed:

#!/bin/bash
# Look for indexBlockData implementations and how 'dataSources' is iterated
rg -nP -C3 '(?:async\s+)?indexBlockData\s*\(.*dataSources\s*:\s*[^\)]*\)' --type ts
rg -nP -C3 '\bfor\s*\(\s*const\s+\w+\s+of\s+dataSources\b|\bdataSources\.forEach\(' --type ts

Length of output: 2595


I need to gather more information about the indexing flow. Let me search for the complete context.

I need to examine the indexBlockData implementation and the filterDataSources logic to verify the claims.

Let me search directly in the codebase for the relevant implementations.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find indexBlockData implementation with context
rg -nA20 'indexBlockData' packages/node-core/src/indexer/indexer.manager.ts --type ts

Length of output: 2063


🏁 Script executed:

#!/bin/bash
# Find filterDataSources implementation with endBlock logic
rg -nA15 'filterDataSources' packages/node-core/src/indexer/indexer.manager.ts --type ts

Length of output: 2146


🏁 Script executed:

#!/bin/bash
# Get the full context around lines 100-160 in indexer.manager.ts
sed -n '100,160p' packages/node-core/src/indexer/indexer.manager.ts

Length of output: 2380


Let me check for all implementations of indexBlockData to ensure the pattern is consistent:


🏁 Script executed:

#!/bin/bash
# Find all indexBlockData implementations
rg -nB2 -A10 'indexBlockData\s*\(' packages/ --type ts -g '!*.spec.ts' | grep -A10 'for\|forEach'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify when vm.freeze callbacks are executed - search for where handlers use injected functions
rg -nC5 'destroyDynamicDatasource|createDynamicDatasource' packages/ --type ts -g '!*.spec.ts' | head -60

Length of output: 4941


The review comment is verified as correct and identifies a real bug.

Based on my analysis of the codebase:

  1. Reassignment ineffectiveness confirmed: The indexBlockData method iterates over filteredDataSources with for...of (line 193). Reassigning the variable inside the callback doesn't affect the ongoing iteration—only in-place modifications (like push at line 115) would work during iteration.

  2. Filter logic issue confirmed: The filterDataSources condition at line 158 is (ds.endBlock ?? Number.MAX_SAFE_INTEGER) >= nextProcessingHeight. When a DS is destroyed with endBlock === blockHeight, this condition evaluates to true, so the destroyed DS is NOT excluded for the current block.

  3. Fix is sound: Using splice() with indexOf() to remove the destroyed DS in-place would correctly remove it from the array during iteration, affecting subsequent handler executions in the same block.

🤖 Prompt for AI Agents
In packages/node-core/src/indexer/indexer.manager.ts around lines 124-134, the
current callback reassigns filteredDataSources after destroying a dynamic
datasource which doesn't affect an ongoing for...of iteration; replace that
reassignment with an in-place removal: locate the destroyed datasource in
filteredDataSources by matching templateName and index (or the datasource
identifier used elsewhere) and call filteredDataSources.splice(foundIndex, 1) so
subsequent handlers in the same block see it removed; do not reassign the
variable. Also verify and, if necessary, fix filterDataSources' condition so a
datasource with endBlock === blockHeight is excluded from current processing
(e.g., use > nextProcessingHeight or adjust the comparison) to avoid future
inconsistencies.

✅ Addressed in commit 1382702

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Gmin2 it would be good to address this and to have a test that confirms the correct behaviour

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@Gmin2 Gmin2 force-pushed the dynamic-datasource branch from 2905c79 to 1382702 Compare October 31, 2025 08:41
Copy link

@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: 0

♻️ Duplicate comments (1)
packages/node-core/src/indexer/indexer.manager.ts (1)

124-143: Confirmed: The matching logic is redundant and fragile. Use object reference tracking instead.

The review comment correctly identifies a real issue. The DynamicDsService maintains synchronized _datasources and _datasourceParams arrays indexed by global index. When destroyDynamicDatasource(templateName, blockHeight, index) is called, the index parameter directly identifies the datasource via this._datasourceParams[index].

The current code at lines 132–138 unnecessarily re-matches datasources in filteredDataSources using property comparison (startBlock + serialized options/args), which:

  1. Is redundant: The index already identifies the exact datasource
  2. Fails with property mismatch: Compares (fds as any).options || (fds as any).processor?.options against args — these are distinct properties with unclear relationship
  3. Is brittle: Multiple datasources could share the same startBlock and args; JSON.stringify is order-dependent

Solution: Store the created datasource reference when adding to filteredDataSources, then use it for removal:

        // Inject function to create ds into vm
        vm.freeze(async (templateName: string, args?: Record<string, unknown>) => {
          const newDs = await this.dynamicDsService.createDynamicDatasource({
            templateName,
            args,
            startBlock: blockHeight,
          });
          // Push the newly created dynamic ds to be processed this block on any future extrinsics/events
          filteredDataSources.push(newDs);
          dynamicDsCreated = true;
        }, 'createDynamicDatasource');

        // Inject function to destroy ds into vm
-       vm.freeze(async (templateName: string, index: number) => {
+       const createdDsMap = new Map<number, DS>();
+       vm.freeze(async (templateName: string, index: number) => {
          await this.dynamicDsService.destroyDynamicDatasource(templateName, blockHeight, index);

-         // Remove the destroyed datasource from the current processing array
-         // Find the datasource by matching the global index stored in the service
-         const destroyedDsParam = this.dynamicDsService.getDatasourceParamByIndex(index);
-         if (destroyedDsParam) {
-           const dsIndex = filteredDataSources.findIndex((fds) => {
-             return (
-               fds.startBlock === destroyedDsParam.startBlock &&
-               JSON.stringify((fds as any).options || (fds as any).processor?.options || {}) ===
-                 JSON.stringify(destroyedDsParam.args || {})
-             );
-           });
-           if (dsIndex !== -1) {
-             filteredDataSources.splice(dsIndex, 1);
-           }
-         }
+         // Remove by reference (stored during creation)
+         const destroyed = createdDsMap.get(index);
+         if (destroyed) {
+           const pos = filteredDataSources.indexOf(destroyed);
+           if (pos !== -1) filteredDataSources.splice(pos, 1);
+         }
        }, 'destroyDynamicDatasource');

        // Store reference mapping during creation
-       vm.freeze(async (templateName: string, args?: Record<string, unknown>) => {
+       const originalCreate = async (templateName: string, args?: Record<string, unknown>) => {
          const newDs = await this.dynamicDsService.createDynamicDatasource({
            templateName,
            args,
            startBlock: blockHeight,
          });
          filteredDataSources.push(newDs);
+         const currentIndex = this.dynamicDsService.dynamicDatasources.length - 1;
+         createdDsMap.set(currentIndex, newDs);
          dynamicDsCreated = true;
-       }, 'createDynamicDatasource');
+       };
+       vm.freeze(originalCreate, 'createDynamicDatasource');
🧹 Nitpick comments (1)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)

18-23: Consider adding JSDoc for the endBlock field.

While the implementation is correct, adding documentation would help users understand when and how endBlock is set.

 export interface DatasourceParams {
   templateName: string;
   args?: Record<string, unknown>;
   startBlock: number;
+  /** Block height where this datasource stops processing (set when destroyed) */
   endBlock?: number;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2905c79 and d2a9845.

📒 Files selected for processing (6)
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6 hunks)
  • packages/node-core/src/indexer/dynamic-ds.service.ts (4 hunks)
  • packages/node-core/src/indexer/indexer.manager.ts (1 hunks)
  • packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2 hunks)
  • packages/types-core/src/global.ts (2 hunks)
  • packages/types-core/src/interfaces.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/types-core/src/interfaces.ts
🧰 Additional context used
🧬 Code graph analysis (3)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2)
packages/node-core/src/indexer/dynamic-ds.service.ts (2)
  • DatasourceParams (18-23)
  • IDynamicDsService (25-32)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
packages/types-core/src/global.ts (3)
packages/node-core/src/indexer/dynamic-ds.service.ts (2)
  • destroyDynamicDatasource (134-180)
  • getDynamicDatasources (183-193)
packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2)
  • destroyDynamicDatasource (40-42)
  • getDynamicDatasources (44-46)
packages/types-core/src/interfaces.ts (2)
  • DynamicDatasourceDestructor (5-5)
  • DynamicDatasourceGetter (26-26)
🔇 Additional comments (13)
packages/node-core/src/indexer/indexer.manager.ts (1)

119-122: LGTM: Clean VM injection for template-based datasource queries.

The function correctly delegates to the dynamic datasource service to retrieve datasources by template name.

packages/types-core/src/global.ts (2)

5-5: LGTM: Import statement updated correctly.

Properly imports the new DynamicDatasourceDestructor and DynamicDatasourceGetter types.


15-16: LGTM: Global bindings for dynamic datasource lifecycle.

The new global functions align with the destructor and getter interfaces defined in interfaces.ts.

packages/node-core/src/indexer/dynamic-ds.service.spec.ts (2)

18-20: LGTM: Test helper updated for endBlock support.

The getTemplate method now correctly accepts an endBlock parameter, aligning with the production implementation.


74-457: Excellent test coverage for dynamic datasource destruction.

The test suite comprehensively covers:

  • Basic destruction scenarios
  • Error handling (non-existent, already-destroyed, out-of-bounds indices)
  • Template name validation
  • Global index tracking after destructions
  • Multi-template scenarios
  • EndBlock propagation and metadata persistence
  • In-memory state synchronization

This achieves the ≥80% coverage requirement from issue #2099.

packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (3)

6-21: LGTM: Worker-host interface extended correctly.

The HostDynamicDS interface and hostDynamicDsKeys array properly include the new destroyDynamicDatasource and getDynamicDatasourcesByTemplate bindings.


40-50: LGTM: Worker methods delegate properly to host.

Both destroyDynamicDatasource and getDynamicDatasourcesByTemplate correctly forward calls to the host implementation.


56-58: LGTM: Host function bindings complete.

The dynamicDsHostFunctions correctly binds the new service methods for cross-thread communication.

packages/node-core/src/indexer/dynamic-ds.service.ts (5)

106-122: LGTM: Clean implementation of template-based datasource queries.

The method correctly:

  • Filters by template name and active status (no endBlock)
  • Preserves global indices for destruction calls
  • Returns well-structured DynamicDatasourceInfo objects

130-132: LGTM: Simple and effective index-based lookup.

The optional chaining correctly returns undefined for out-of-bounds indices.


134-180: LGTM: Robust destruction implementation with excellent validation.

The method includes comprehensive checks:

  • Initialization state
  • Index bounds with clear error messages
  • Template name matching
  • Already-destroyed prevention
  • Array synchronization validation

The error messages are clear and actionable. The metadata persistence ensures durability across restarts.


210-217: LGTM: Template construction updated for endBlock support.

The method correctly:

  • Accepts optional endBlock parameter
  • Spreads it into the cloned template
  • Maintains the name removal to avoid pollution

219-229: LGTM: Datasource construction propagates endBlock correctly.

The endBlock is properly passed from params to getTemplate, ensuring consistency throughout the datasource lifecycle.

if (!this._datasources[index]) {
throw new Error(`Datasources array out of sync with params at index ${index}`);
}
(this._datasources[index] as any).endBlock = currentBlockHeight;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can the any cast be removed here please

Comment on lines 124 to 144
// Inject function to destroy ds into vm
vm.freeze(async (templateName: string, index: number) => {
await this.dynamicDsService.destroyDynamicDatasource(templateName, blockHeight, index);

// Re-filter datasources to exclude the destroyed one
// The destroyed datasource now has endBlock set, so filterDataSources will exclude it
// Note: Reassigning filteredDataSources is intentional - subsequent handlers
// within the same block will see the updated filtered list
filteredDataSources = this.filterDataSources(blockHeight, filteredDataSources);
}, 'destroyDynamicDatasource');

Comment on lines 128 to 141
// Remove the destroyed datasource from the current processing array
// Find the datasource by matching the global index stored in the service
const destroyedDsParam = this.dynamicDsService.getDatasourceParamByIndex(index);
if (destroyedDsParam) {
const dsIndex = filteredDataSources.findIndex((fds) => {
return (
fds.startBlock === destroyedDsParam.startBlock &&
JSON.stringify((fds as any).options || (fds as any).processor?.options || {}) ===
JSON.stringify(destroyedDsParam.args || {})
);
});
if (dsIndex !== -1) {
filteredDataSources.splice(dsIndex, 1);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be moved into its own function.

const dsIndex = filteredDataSources.findIndex((fds) => {
return (
fds.startBlock === destroyedDsParam.startBlock &&
JSON.stringify((fds as any).options || (fds as any).processor?.options || {}) ===
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is quite fragile, there's no requirement that args maps to options or processor.options which would mean this fails. A better option would be to get the dynamic datasource rather than just the params and use that to compare.

The as any checks should also be removed.

@Gmin2 Gmin2 force-pushed the dynamic-datasource branch from d2a9845 to 87dca65 Compare December 1, 2025 22:02
@Gmin2
Copy link
Contributor Author

Gmin2 commented Dec 1, 2025

Take a look @stwiname

Copy link

@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: 0

♻️ Duplicate comments (1)
packages/node-core/src/indexer/indexer.manager.ts (1)

124-129: Destroyed datasource still processes remaining handlers in the same block.

Per the past review discussion with @stwiname, setting endBlock alone doesn't prevent the destroyed datasource from processing remaining handlers within the same block. The filterDataSources condition at line 154 uses >= nextProcessingHeight, so a datasource with endBlock === blockHeight is still included.

The comment indicates child classes should check ds.endBlock, but this requires coordination across all implementations. Consider:

  1. Removing the destroyed DS in-place from filteredDataSources using splice() after destruction, or
  2. Adding a test that confirms and documents the intended behavior (that destruction takes effect on the next block, not immediately)
#!/bin/bash
# Check if child implementations check endBlock before processing handlers
rg -nC5 'indexBlockData|endBlock' packages/node/src/indexer/ packages/node-core/src/indexer/ --type ts -g '!*.spec.ts' | head -80
🧹 Nitpick comments (3)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)

176-178: Consider adding endBlock to BaseDataSource type for cleaner typing.

The Object.assign workaround is functional but the comment acknowledges the type mismatch. If BaseDataSource in @subql/types-core doesn't include endBlock, consider extending the type or using a union type to avoid runtime property assignment that bypasses TypeScript's type system.

For now, the implementation is correct - just a suggestion for future type safety improvements.

packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (1)

42-56: Consider serialization consistency for cross-thread communication.

createDynamicDatasource (line 39) uses JSON.parse(JSON.stringify(params)) for serialization over the worker bridge. The new methods don't apply the same pattern. While this may be fine if the worker thread uses a shared memory bridge or if the data is already serializable, consider whether getDatasourceParamByIndex should also ensure its return value is properly serialized.

If the bridge handles serialization automatically, this is fine as-is.

packages/node/src/indexer/indexer.manager.ts (1)

201-202: Add endBlock property to type definition for better type safety.

The type assertion (ds as { endBlock?: number }) at lines 201-202 bypasses TypeScript type checking. Since endBlock is being dynamically set and accessed in this lifecycle management feature, consider adding endBlock?: number to the SubstrateProjectDs type definition to maintain type safety throughout the codebase. Alternatively, use a type guard function instead of type assertion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2a9845 and 87dca65.

📒 Files selected for processing (5)
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6 hunks)
  • packages/node-core/src/indexer/dynamic-ds.service.ts (4 hunks)
  • packages/node-core/src/indexer/indexer.manager.ts (1 hunks)
  • packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2 hunks)
  • packages/node/src/indexer/indexer.manager.ts (6 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript throughout with strict configuration in the monorepo
Use Ethers.js for Ethereum integration

Files:

  • packages/node/src/indexer/indexer.manager.ts
  • packages/node-core/src/indexer/indexer.manager.ts
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
  • packages/node-core/src/indexer/dynamic-ds.service.ts
  • packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run ESLint with TypeScript support, Prettier, and various plugins across all TypeScript files using configured husky pre-commit hooks and lint-staged

Files:

  • packages/node/src/indexer/indexer.manager.ts
  • packages/node-core/src/indexer/indexer.manager.ts
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
  • packages/node-core/src/indexer/dynamic-ds.service.ts
  • packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use custom Jest configuration with module name mapping for workspace packages, enforce UTC timezone for tests, and handle Polkadot packages in transform ignore patterns

Files:

  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
🧬 Code graph analysis (3)
packages/node/src/indexer/indexer.manager.ts (2)
packages/node/src/indexer/types.ts (1)
  • isFullBlock (31-35)
packages/common-substrate/src/project/versioned/ProjectManifestVersioned.ts (1)
  • dataSources (54-56)
packages/node-core/src/indexer/dynamic-ds.service.spec.ts (1)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
  • DatasourceParams (18-23)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
🔇 Additional comments (13)
packages/node-core/src/indexer/indexer.manager.ts (1)

119-122: LGTM!

The getDynamicDatasources injection follows the established pattern and correctly delegates to the service method.

packages/node-core/src/indexer/dynamic-ds.service.spec.ts (5)

18-27: LGTM!

The test helper correctly mirrors the updated getTemplate signature, and testParamOther enables thorough multi-template testing.


74-136: Good test coverage for destruction scenarios.

The tests thoroughly verify:

  • EndBlock is correctly set on both params and datasource objects
  • Error handling for non-existent templates and already-destroyed datasources
  • Lifecycle flow of destroy-then-create

150-168: LGTM!

The test correctly verifies that resetDynamicDatasource preserves the endBlock on already-destroyed datasources while removing those created after the target height.


263-333: Comprehensive test coverage for getDynamicDatasourcesByTemplate.

The tests properly validate:

  • Global index preservation after destruction
  • Destroyed datasource exclusion
  • Args propagation
  • Initialization requirements

335-458: Thorough test coverage for index-based destruction.

The tests comprehensively cover:

  • Boundary validation (out of bounds, negative indices)
  • Global index semantics after partial destruction
  • Cross-template independence
  • Template name validation at specific indices
  • EndBlock propagation to both params and datasource objects
packages/node-core/src/indexer/dynamic-ds.service.ts (4)

98-122: LGTM!

Clean implementation that correctly filters active datasources by template and provides global indices for subsequent destruction operations.


124-132: LGTM!

Concise implementation with proper optional chaining.


213-219: LGTM!

Clean backward-compatible extension that includes endBlock in the datasource creation.


222-224: LGTM!

Correctly propagates endBlock from params during datasource reconstruction.

packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts (2)

9-23: LGTM!

The host interface and keys correctly mirror the new IDynamicDsService methods.


59-66: LGTM!

Host functions correctly bind all new service methods.

packages/node/src/indexer/indexer.manager.ts (1)

203-204: Clarify the destruction timing semantics.

The condition endBlock <= blockHeight will skip a datasource at the block where it's destroyed. The comment states "exclude it from processing in subsequent handlers within the same block," which aligns with this logic.

However, please verify the intended behavior:

  • When a datasource is destroyed during block N processing (e.g., in an early handler), should handlers that run LATER in the same block still process that datasource?
  • The current implementation will skip it for all handlers after destruction in the same block, which appears intentional but should be confirmed.

If the datasource should remain active for handlers executed BEFORE the destruction call within the same block, the current approach is correct. If it should be excluded entirely from block N when destroyed at block N, this is also correct. Please confirm this is the desired behavior and consider adding a test case that validates destruction timing within a single block.

Copy link
Collaborator

@stwiname stwiname left a comment

Choose a reason for hiding this comment

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

I still don't see a test that addresses this case. #2904 (comment)

Comment on lines 198 to 205
// Skip datasources that have been destroyed at or before this block
// When a datasource is destroyed, its endBlock is set to the current blockHeight
// We want to exclude it from processing in subsequent handlers within the same block
const endBlock =
'endBlock' in ds ? (ds as { endBlock?: number }).endBlock : undefined;
if (endBlock !== undefined && endBlock <= blockHeight) {
continue;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a bad place to do this, the provided dataSources should already have the filtered dataSources to be relevant.

Having this here makes it really hard to find as it's separate from the rest of the filtering

Copy link
Contributor Author

@Gmin2 Gmin2 Dec 4, 2025

Choose a reason for hiding this comment

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

Now all filtering happens centrally in filterDataSources method in filterDataSources method and the VM callback calls it to reassign the filtered datasources.

@Gmin2 Gmin2 force-pushed the dynamic-datasource branch from 2d35591 to 5cf5af1 Compare December 4, 2025 08:16
Copy link

@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: 1

🧹 Nitpick comments (2)
packages/node/src/indexer/indexer.manager.ts (1)

184-202: Unused blockHeight parameter and misleading comment.

The blockHeight parameter is passed to indexContent but never used within the returned function. The comment on line 197 states datasources are "removed from the array and the loop exits early" when destroyed, but the implementation shows a standard iteration without any early-exit logic or array modification handling.

Based on the relevant code snippet from packages/node-core/src/indexer/indexer.manager.ts, filtering of destroyed datasources happens in filterDataSources before datasources reach this manager. If that's the intended design:

  1. Remove the unused blockHeight parameter from indexContent
  2. Update the comment to accurately reflect the behavior
  private indexContent(
    kind: SubstrateHandlerKind,
-   blockHeight: number,
  ): (
    content:
      | SubstrateBlock
      | SubstrateExtrinsic
      | SubstrateEvent
      | LightSubstrateEvent,
    dataSources: SubstrateProjectDs[],
    getVM: (d: SubstrateProjectDs) => Promise<IndexerSandbox>,
  ) => Promise<void> {
    return async (content, dataSources, getVM) => {
-     // When a datasource is destroyed, its removed from the array and the loop exits early
      for (let i = 0; i < dataSources.length; i++) {
        const ds = dataSources[i];
        await this.indexData(kind, content, ds, getVM);
      }
    };
  }
packages/node-core/src/indexer/dynamic-ds.service.spec.ts (1)

459-494: Consider adding integration test for actual filtering path.

This test simulates the filterDataSources logic locally rather than exercising the actual production code path in indexer.manager.ts. While this validates the expected contract, an integration test using the real filterDataSources method would provide stronger guarantees.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d35591 and 5cf5af1.

📒 Files selected for processing (4)
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6 hunks)
  • packages/node-core/src/indexer/dynamic-ds.service.ts (4 hunks)
  • packages/node-core/src/indexer/indexer.manager.ts (3 hunks)
  • packages/node/src/indexer/indexer.manager.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/node-core/src/indexer/indexer.manager.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript throughout with strict configuration in the monorepo
Use Ethers.js for Ethereum integration

Files:

  • packages/node/src/indexer/indexer.manager.ts
  • packages/node-core/src/indexer/dynamic-ds.service.ts
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run ESLint with TypeScript support, Prettier, and various plugins across all TypeScript files using configured husky pre-commit hooks and lint-staged

Files:

  • packages/node/src/indexer/indexer.manager.ts
  • packages/node-core/src/indexer/dynamic-ds.service.ts
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use custom Jest configuration with module name mapping for workspace packages, enforce UTC timezone for tests, and handle Polkadot packages in transform ignore patterns

Files:

  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
🧬 Code graph analysis (2)
packages/node-core/src/indexer/dynamic-ds.service.ts (2)
packages/types-core/src/interfaces.ts (1)
  • DynamicDatasourceInfo (10-24)
packages/node-core/logger.js (1)
  • logger (5-5)
packages/node-core/src/indexer/dynamic-ds.service.spec.ts (2)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
  • DatasourceParams (18-23)
packages/node-core/src/indexer/indexer.manager.ts (1)
  • filterDataSources (148-168)
🔇 Additional comments (12)
packages/node/src/indexer/indexer.manager.ts (1)

100-101: Block height extraction is correct but may be unused.

The extraction logic is correct. However, if the blockHeight parameter is removed from indexContent as suggested above, this variable and the comment become unnecessary.

packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6)

18-20: LGTM!

The test helper correctly exposes getTemplate with the new optional endBlock parameter, maintaining consistency with the production API changes.


23-28: LGTM!

Test fixtures are well-structured with multiple templates supporting comprehensive cross-template destruction testing.

Also applies to: 41-49


74-135: LGTM!

Thorough test coverage for destruction lifecycle including error cases and recovery scenarios. The tests correctly validate endBlock propagation to both _datasourceParams and _datasources internal arrays.


150-168: LGTM!

The reset test correctly verifies that destroyed datasources retain their endBlock after a resetDynamicDatasource call.


263-333: LGTM!

Comprehensive coverage for getDynamicDatasourcesByTemplate including global index tracking, destroyed datasource exclusion, and initialization checks.


495-527: Test pattern doesn't match actual indexer implementation.

This test demonstrates array reassignment during iteration, but the actual indexContent loop in packages/node/src/indexer/indexer.manager.ts doesn't implement this re-filtering pattern—it just iterates normally. Either update the indexer manager to match this pattern or adjust test expectations.

packages/node-core/src/indexer/dynamic-ds.service.ts (5)

5-5: LGTM!

Import correctly added to support the new DynamicDatasourceInfo return type.


18-32: LGTM!

Interface properly extended with endBlock field and new method signatures. The index parameter is now required as suggested in past review comments.


98-122: LGTM!

Clean implementation that correctly filters active datasources and provides global indices for destruction operations. The documentation clearly explains the index field usage.


124-132: LGTM!

Clean implementation using optional chaining as suggested in past review comments.


210-222: LGTM!

getTemplate and getDatasource correctly propagate endBlock through the template construction chain. The implementation aligns with the past review comment noting that BaseDataSource includes endBlock.

Comment on lines +165 to +179
// Update the datasource params
const updatedParams = {...dsParam, endBlock: currentBlockHeight};
this._datasourceParams[index] = updatedParams;

// Update the datasource object if it exists
// Note: _datasources and _datasourceParams arrays should always be in sync.
// If the index is valid for params, it must also be valid for datasources.
const datasource = this._datasources[index];
if (!datasource) {
throw new Error(`Datasources array out of sync with params at index ${index}`);
}
// Set endBlock on the datasource object
datasource.endBlock = currentBlockHeight;

await this.metadata.set(METADATA_KEY, this._datasourceParams, tx);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

In-memory state updated before persistence could cause inconsistency.

If metadata.set fails, the in-memory state (_datasourceParams[index] and _datasources[index].endBlock) is already modified, leaving the service in an inconsistent state. Compare with createDynamicDatasource which persists to metadata before updating in-memory state.

Consider reordering to persist first:

-   // Update the datasource params
-   const updatedParams = {...dsParam, endBlock: currentBlockHeight};
-   this._datasourceParams[index] = updatedParams;
-
-   // Update the datasource object if it exists
-   const datasource = this._datasources[index];
-   if (!datasource) {
-     throw new Error(`Datasources array out of sync with params at index ${index}`);
-   }
-   // Set endBlock on the datasource object
-   datasource.endBlock = currentBlockHeight;
-
-   await this.metadata.set(METADATA_KEY, this._datasourceParams, tx);
+   // Prepare updated params
+   const updatedParams = {...dsParam, endBlock: currentBlockHeight};
+
+   // Create a copy for persistence
+   const paramsForPersistence = [...this._datasourceParams];
+   paramsForPersistence[index] = updatedParams;
+
+   // Persist first, then update in-memory on success
+   await this.metadata.set(METADATA_KEY, paramsForPersistence, tx);
+
+   // Update in-memory state only after successful persistence
+   this._datasourceParams[index] = updatedParams;
+
+   const datasource = this._datasources[index];
+   if (!datasource) {
+     throw new Error(`Datasources array out of sync with params at index ${index}`);
+   }
+   datasource.endBlock = currentBlockHeight;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
packages/node-core/src/indexer/dynamic-ds.service.ts around lines 165 to 179:
the code mutates in-memory state (_datasourceParams[index] and
_datasources[index].endBlock) before calling metadata.set, which can leave the
service inconsistent if persistence fails; instead, build the updated params
object/array but do NOT assign it into _datasourceParams or modify
datasource.endBlock until metadata.set(METADATA_KEY, updatedParamsArray, tx)
completes successfully; after metadata.set resolves, then assign
this._datasourceParams[index] = updatedParams and set
this._datasources[index].endBlock = currentBlockHeight (and keep throwing the
same error if datasource is missing before doing any in-memory mutation).

Copy link

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cf5af1 and efb5b82.

📒 Files selected for processing (2)
  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts (6 hunks)
  • packages/node-core/src/indexer/indexer.manager.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/node-core/src/indexer/indexer.manager.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript throughout with strict configuration in the monorepo
Use Ethers.js for Ethereum integration

Files:

  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use custom Jest configuration with module name mapping for workspace packages, enforce UTC timezone for tests, and handle Polkadot packages in transform ignore patterns

Files:

  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run ESLint with TypeScript support, Prettier, and various plugins across all TypeScript files using configured husky pre-commit hooks and lint-staged

Files:

  • packages/node-core/src/indexer/dynamic-ds.service.spec.ts
🧬 Code graph analysis (1)
packages/node-core/src/indexer/dynamic-ds.service.spec.ts (2)
packages/node-core/src/indexer/dynamic-ds.service.ts (1)
  • DatasourceParams (18-23)
packages/node-core/src/indexer/indexer.manager.ts (1)
  • filterDataSources (148-168)
🔇 Additional comments (7)
packages/node-core/src/indexer/dynamic-ds.service.spec.ts (7)

18-20: LGTM! Test infrastructure properly extended for endBlock feature.

The getTemplate signature update and multi-template test setup align well with the PR objectives for dynamic datasource destruction with endBlock support.

Also applies to: 27-27, 44-44


150-203: LGTM! Reset and loading tests properly validate endBlock persistence.

These tests correctly verify that:

  • Destroyed datasources with endBlock are preserved during resets
  • endBlock is correctly loaded from metadata and reflected in datasource objects

205-237: LGTM! Metadata and template tests validate correct behavior.

The tests properly verify:

  • Metadata updates when destroying datasources
  • Templates can be created with endBlock parameter
  • Template immutability is preserved

239-260: LGTM! Multi-template and initialization tests cover important edge cases.

These tests validate:

  • Index-based destruction works correctly when multiple datasources share a template name
  • Proper error handling when attempting operations on uninitialized service

262-332: LGTM! Comprehensive test coverage for getDynamicDatasourcesByTemplate.

This test suite thoroughly validates:

  • Retrieval of active datasources filtered by template name
  • Exclusion of destroyed datasources
  • Global index tracking
  • Args propagation
  • Error handling for uninitialized service

458-526: LGTM! Excellent integration tests for block processing scenarios.

These tests thoroughly validate:

  • Filtering datasources by endBlock during block processing (mimicking the actual filterDataSources logic from indexer.manager.ts)
  • In-place datasource removal during block processing
  • Traditional for-loop pattern with array reassignment after destruction

The inline filter function at lines 467-472 correctly mirrors the actual filtering logic, ensuring the destruction feature integrates properly with existing block processing.


438-456: Verify getDatasourceParamByIndex method exists.

Line 446 calls service.getDatasourceParamByIndex(1), but this method is not visible in the test file or the TestDynamicDsService class definition. Ensure this method exists in the DynamicDsService base class.

Comment on lines +108 to +135
it('allows creating new datasource after destroying existing one', async () => {
const meta = mockMetadata([testParam1]);
await service.init(meta);

expect((service as any)._datasourceParams).toEqual([testParam1]);

// Destroy by index
await service.destroyDynamicDatasource('Test', 50, 0);

const paramsAfterDestroy = (service as any)._datasourceParams;
expect(paramsAfterDestroy[0]).toEqual({...testParam1, endBlock: 50});

const newParam = {templateName: 'Test', startBlock: 60};
await service.createDynamicDatasource(newParam);

const finalParams: DatasourceParams[] = (service as any)._datasourceParams;
const destroyedCount = finalParams.filter((p: DatasourceParams) => p.endBlock !== undefined).length;
const activeCount = finalParams.filter((p: DatasourceParams) => p.endBlock === undefined).length;

expect(destroyedCount).toBeGreaterThanOrEqual(1);
expect(activeCount).toBeGreaterThanOrEqual(1);

const destroyedParam = finalParams.find((p: DatasourceParams) => p.startBlock === 1 && p.endBlock === 50);
expect(destroyedParam).toBeDefined();

const newParamFound = finalParams.find((p: DatasourceParams) => p.startBlock === 60 && !p.endBlock);
expect(newParamFound).toBeDefined();
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Strengthen count assertions for precision.

Lines 127-128 use toBeGreaterThanOrEqual(1), but after destroying one datasource and creating one new datasource, the counts should be exact: 1 destroyed and 1 active. Weak assertions may miss bugs where extra datasources are inadvertently created or destroyed.

Apply this diff:

-    expect(destroyedCount).toBeGreaterThanOrEqual(1);
-    expect(activeCount).toBeGreaterThanOrEqual(1);
+    expect(destroyedCount).toBe(1);
+    expect(activeCount).toBe(1);
🤖 Prompt for AI Agents
In packages/node-core/src/indexer/dynamic-ds.service.spec.ts around lines 108 to
135, the test uses weak assertions (toBeGreaterThanOrEqual(1)) for
destroyedCount and activeCount which should be exact after destroying one
datasource and creating one new one; change those two assertions to
expect(destroyedCount).toBe(1) and expect(activeCount).toBe(1) so the test
verifies exactly one destroyed and one active datasource, preventing unnoticed
extra creations or destructions.

@Gmin2
Copy link
Contributor Author

Gmin2 commented Dec 4, 2025

I still don't see a test that addresses this case. #2904 (comment)

Added two test case in 5cf5af1 to check the filtering logic or am I missing something really obivious ones

@Gmin2 Gmin2 requested a review from stwiname December 4, 2025 08:25
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.

[$300 in SQT] Dynamic Datasources destructor

2 participants