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

    • Add ability to destroy dynamic datasources by template and index, and to query active datasources by template.
    • Introduce optional endBlock on datasources for explicit lifecycle termination and improved indexing consistency.
    • Expose destroy/get dynamic-datasource operations to runtime/worker host functions and global bindings for in-block updates.
  • Tests

    • Expanded tests covering destruction flows, endBlock behavior, template-scoped queries, error cases, and in-memory state consistency.

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 dynamic datasource destruction and retrieval by template: datasources gain optional endBlock; services, indexer manager, worker host and VM bindings expose destroy/get operations by index/template; tests expanded to cover destruction, endBlock propagation, and in-memory/persisted metadata consistency.

Changes

Cohort / File(s) Change Summary
Type Definitions
packages/types-core/src/interfaces.ts
Added DynamicDatasourceDestructor, DynamicDatasourceGetter, and DynamicDatasourceInfo exports describing destructor signature and datasource metadata (index, templateName, startBlock, optional endBlock, args).
Global API Exports
packages/types-core/src/global.ts
Exposed new global bindings destroyDynamicDatasource and getDynamicDatasources (types from interfaces) for handler VM.
Core Service Implementation
packages/node-core/src/indexer/dynamic-ds.service.ts
Added endBlock?: number to DatasourceParams; extended IDynamicDsService and DynamicDsService with destroyDynamicDatasource, getDynamicDatasourcesByTemplate, and getDatasourceParamByIndex; propagated endBlock through getTemplate/getDatasource; implemented destruction that sets endBlock, updates datasource objects, persists metadata, and validates sync between params and datasource lists.
Indexer Manager / VM Sandbox
packages/node-core/src/indexer/indexer.manager.ts
Injected getDynamicDatasources(templateName) and destroyDynamicDatasource(templateName, index) into VM sandbox; on destruction re-filters filteredDataSources so subsequent handlers in the same block observe the updated set.
Worker Thread Support
packages/node-core/src/indexer/worker/worker.dynamic-ds.service.ts
Extended HostDynamicDS interface and hostDynamicDsKeys with destroy/get methods; added destroyDynamicDatasource and getDynamicDatasourcesByTemplate on WorkerDynamicDsService and bound them in dynamicDsHostFunctions.
Tests
packages/node-core/src/indexer/dynamic-ds.service.spec.ts
Expanded test project templates to include a second template and testParamOther; updated test helper getTemplate signature to accept endBlock; added tests covering destruction (valid, non-existent, already-destroyed), creation after destruction, reset behavior, endBlock propagation, multi-template indexing, and in-memory _datasourceParams/_datasources consistency.

Sequence Diagram(s)

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

    Handler->>VM: destroyDynamicDatasource(templateName, index)
    VM->>Manager: destroyDynamicDatasource(templateName, index)
    Manager->>Service: destroyDynamicDatasource(templateName, currentBlockHeight, index)
    Service->>Service: getDatasourceParamByIndex(index)
    alt exists & active
        Service->>Service: set param.endBlock = currentBlockHeight
        Service->>Service: update in-memory datasource (endBlock)
        Service->>DB: persist updated params (metadata)
        Service-->>Manager: success
        Manager->>Manager: re-filter filteredDataSources (apply endBlock)
        Manager-->>VM: ack
        VM-->>Handler: continue (datasource no longer active)
    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

Potential focus areas:

  • Synchronization between _datasourceParams and _datasources when setting endBlock.
  • Persistence format and correctness when writing updated params to metadata store.
  • Correct index semantics (global index vs. template-local index) and error messaging.
  • VM/worker host binding names and cross-thread delegation correctness.

Poem

🐇 I mark the end, a gentle tap,

a hop, a thump, the datasource nap.
Templates tidy, indices neat,
metadata hums its steady beat.
Hooray — I hop, the state’s complete!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
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.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: implemented dynamic datasource removing themseleves and adding them at end blocks" is clearly and specifically related to the main changes in the pull request. It references both key features being introduced: the ability for dynamic datasources to remove themselves and the support for end blocks. The title is concise and would help developers scanning the repository history understand the primary change at a glance. While there is a minor typo ("themseleves" instead of "themselves"), this does not impair the clarity or relevance of the title to the changeset.
Linked Issues Check ✅ Passed The PR successfully implements the core coding requirements from issue #2099. The implementation provides the ability for dynamic datasources to remove themselves via the new destroyDynamicDatasource methods added to IDynamicDsService and DynamicDsService, with endBlock field support in DatasourceParams. Query functionality is exposed through getDynamicDatasourcesByTemplate and getDatasourceParamByIndex methods. Extensive tests have been added to validate the new destruction behavior, endBlock propagation, and multi-template scenarios. The PR integrates these features across the indexer manager, worker services, and global type definitions. Non-coding requirements such as documentation updates, codegen modifications, and example projects are outside the scope of the code changes in this PR and would typically be addressed separately or in follow-up tasks.
Out of Scope Changes Check ✅ Passed All code changes in the PR are directly aligned with the dynamic datasource destruction feature implementation. The modifications include test expansions in the spec file, core implementation of destruction and query methods in the service files, integration of VM-accessible functions in the indexer manager, worker service extensions to delegate the new functionality, and type definitions for the public API surface. No unrelated refactoring, bug fixes in other features, formatting-only changes, or extraneous modifications are present in the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

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