-
Notifications
You must be signed in to change notification settings - Fork 395
feat: implemented dynamic datasource removing themseleves and adding them at end blocks #2904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
78f8551
323a622
4971c3d
af7339f
cd60d99
54eb623
1382702
87dca65
5cf5af1
efb5b82
a386dc4
de059a3
78b5091
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,15 +15,16 @@ class TestDynamicDsService extends DynamicDsService<BaseDataSource, ISubqueryPro | |
| } | ||
|
|
||
| // Make it public | ||
| getTemplate(templateName: string, startBlock?: number | undefined): BaseDataSource { | ||
| return super.getTemplate(templateName, startBlock); | ||
| getTemplate(templateName: string, startBlock?: number | undefined, endBlock?: number | undefined): BaseDataSource { | ||
| return super.getTemplate(templateName, startBlock, endBlock); | ||
| } | ||
| } | ||
|
|
||
| const testParam1 = {templateName: 'Test', startBlock: 1}; | ||
| const testParam2 = {templateName: 'Test', startBlock: 2}; | ||
| const testParam3 = {templateName: 'Test', startBlock: 3}; | ||
| const testParam4 = {templateName: 'Test', startBlock: 4}; | ||
| const testParamOther = {templateName: 'Other', startBlock: 5}; | ||
|
|
||
| const mockMetadata = (initData: DatasourceParams[] = []) => { | ||
| let datasourceParams: DatasourceParams[] = initData; | ||
|
|
@@ -40,7 +41,7 @@ const mockMetadata = (initData: DatasourceParams[] = []) => { | |
| describe('DynamicDsService', () => { | ||
| let service: TestDynamicDsService; | ||
| const project = { | ||
| templates: [{name: 'Test'}], | ||
| templates: [{name: 'Test'}, {name: 'Other'}], | ||
| } as any as ISubqueryProject; | ||
|
|
||
| beforeEach(() => { | ||
|
|
@@ -70,6 +71,69 @@ describe('DynamicDsService', () => { | |
| ]); | ||
| }); | ||
|
|
||
| it('can destroy a dynamic datasource', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2]); | ||
| await service.init(meta); | ||
|
|
||
| // Destroy specific datasource by index | ||
| await service.destroyDynamicDatasource('Test', 50, 0); | ||
|
|
||
| const updatedParams = (service as any)._datasourceParams; | ||
| expect(updatedParams[0]).toEqual({...testParam1, endBlock: 50}); | ||
| expect(updatedParams[1]).toEqual(testParam2); | ||
|
|
||
| const datasources = (service as any)._datasources; | ||
| expect(datasources[0].endBlock).toBe(50); | ||
| }); | ||
|
|
||
| it('throws error when destroying non-existent datasource', async () => { | ||
| const meta = mockMetadata([testParam1]); | ||
| await service.init(meta); | ||
|
|
||
| await expect(service.destroyDynamicDatasource('NonExistent', 50, 0)).rejects.toThrow( | ||
| 'Datasource at index 0 has template name "Test", not "NonExistent"' | ||
| ); | ||
| }); | ||
|
|
||
| it('throws error when destroying already destroyed datasource', async () => { | ||
| const destroyedParam = {...testParam1, endBlock: 30}; | ||
| const meta = mockMetadata([destroyedParam]); | ||
| await service.init(meta); | ||
|
|
||
| await expect(service.destroyDynamicDatasource('Test', 50, 0)).rejects.toThrow( | ||
| 'Dynamic datasource at index 0 is already destroyed' | ||
| ); | ||
| }); | ||
|
|
||
| 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 = (service as any)._datasourceParams; | ||
| const destroyedCount = finalParams.filter((p) => p.endBlock !== undefined).length; | ||
| const activeCount = finalParams.filter((p) => p.endBlock === undefined).length; | ||
|
|
||
| expect(destroyedCount).toBeGreaterThanOrEqual(1); | ||
| expect(activeCount).toBeGreaterThanOrEqual(1); | ||
|
|
||
| const destroyedParam = finalParams.find((p) => p.startBlock === 1 && p.endBlock === 50); | ||
| expect(destroyedParam).toBeDefined(); | ||
|
|
||
| const newParamFound = finalParams.find((p) => p.startBlock === 60 && !p.endBlock); | ||
| expect(newParamFound).toBeDefined(); | ||
| }); | ||
|
|
||
| it('resets dynamic datasources', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParam3, testParam4]); | ||
| await service.init(meta); | ||
|
|
@@ -83,6 +147,26 @@ describe('DynamicDsService', () => { | |
| ]); | ||
| }); | ||
|
|
||
| it('handles reset after datasource destruction correctly', async () => { | ||
| const params = [testParam1, testParam2, testParam3, testParam4]; | ||
| const meta = mockMetadata(params); | ||
| await service.init(meta); | ||
|
|
||
| // Destroy only the first datasource by index | ||
| await service.destroyDynamicDatasource('Test', 25, 0); | ||
|
|
||
| const paramsAfterDestroy = (service as any)._datasourceParams; | ||
| expect(paramsAfterDestroy[0]).toEqual({...testParam1, endBlock: 25}); | ||
|
|
||
| // Reset to block 2 (should keep testParam1 and testParam2) | ||
| await service.resetDynamicDatasource(2, null as any); | ||
|
|
||
| const paramsAfterReset = (service as any)._datasourceParams; | ||
| expect(paramsAfterReset).toHaveLength(2); | ||
| expect(paramsAfterReset[0]).toEqual({...testParam1, endBlock: 25}); | ||
| expect(paramsAfterReset[1]).toEqual(testParam2); | ||
| }); | ||
|
|
||
| it('getDynamicDatasources with force reloads from metadata', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2]); | ||
| await service.init(meta); | ||
|
|
@@ -107,6 +191,30 @@ describe('DynamicDsService', () => { | |
| ]); | ||
| }); | ||
|
|
||
| it('loads destroyed datasources with endBlock correctly', async () => { | ||
| const destroyedParam = {...testParam1, endBlock: 100}; | ||
| const meta = mockMetadata([destroyedParam, testParam2]); | ||
| await service.init(meta); | ||
|
|
||
| const datasources = await service.getDynamicDatasources(); | ||
| expect(datasources).toHaveLength(2); | ||
| expect((datasources[0] as any).endBlock).toBe(100); | ||
| expect((datasources[1] as any).endBlock).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('updates metadata correctly when destroying datasource', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2]); | ||
| await service.init(meta); | ||
|
|
||
| // Destroy first datasource by index | ||
| await service.destroyDynamicDatasource('Test', 75, 0); | ||
|
|
||
| const metadataParams = await meta.find('dynamicDatasources'); | ||
| expect(metadataParams).toBeDefined(); | ||
| expect(metadataParams![0]).toEqual({...testParam1, endBlock: 75}); | ||
| expect(metadataParams![1]).toEqual(testParam2); | ||
|
Comment on lines
+205
to
+215
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This metadata assertion is currently non-diagnostic. These checks depend on 🧪 One way to make the fixture independentconst cloneParam = (param: DatasourceParams): DatasourceParams => ({
...param,
args: param.args ? {...param.args} : undefined,
});
const mockMetadata = (initData: DatasourceParams[] = []) => {
let datasourceParams: DatasourceParams[] = initData.map(cloneParam);
return {
set: (_key: string, value: DatasourceParams[]) => {
datasourceParams = value.map(cloneParam);
},
find: (_key: string) => Promise.resolve(datasourceParams.map(cloneParam)),
setNewDynamicDatasource: (params: DatasourceParams) => datasourceParams.push(cloneParam(params)),
} as unknown as CacheMetadataModel;
};🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| it('can find a template and cannot mutate the template', () => { | ||
| const template1 = service.getTemplate('Test', 1); | ||
| const template2 = service.getTemplate('Test', 2); | ||
|
|
@@ -120,4 +228,212 @@ describe('DynamicDsService', () => { | |
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| expect(project.templates![0]).toEqual({name: 'Test'}); | ||
| }); | ||
|
|
||
| it('can create template with endBlock', () => { | ||
| const template = service.getTemplate('Test', 1, 100); | ||
|
|
||
| expect(template.startBlock).toBe(1); | ||
| expect((template as any).endBlock).toBe(100); | ||
| expect((template as any).name).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('handles multiple templates with same name during destruction', async () => { | ||
| const param1 = {templateName: 'Test', startBlock: 1}; | ||
| const param2 = {templateName: 'Test', startBlock: 5}; | ||
| const param3 = {templateName: 'Other', startBlock: 3}; | ||
|
|
||
| const meta = mockMetadata([param1, param2, param3]); | ||
| await service.init(meta); | ||
|
|
||
| // Should destroy the first matching one by index | ||
| await service.destroyDynamicDatasource('Test', 10, 0); | ||
|
|
||
| const updatedParams = (service as any)._datasourceParams; | ||
| expect(updatedParams[0]).toEqual({...param1, endBlock: 10}); | ||
| expect(updatedParams[1]).toEqual(param2); // Not destroyed | ||
| expect(updatedParams[2]).toEqual(param3); // Not destroyed | ||
| }); | ||
|
|
||
| it('throws error when service not initialized for destruction', async () => { | ||
| await expect(service.destroyDynamicDatasource('Test', 50, 0)).rejects.toThrow( | ||
| 'DynamicDsService has not been initialized' | ||
| ); | ||
| }); | ||
|
|
||
| describe('getDynamicDatasourcesByTemplate', () => { | ||
| it('returns list of active datasources for a template', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParam3, testParamOther]); | ||
| await service.init(meta); | ||
|
|
||
| const testDatasources = service.getDynamicDatasourcesByTemplate('Test'); | ||
|
|
||
| expect(testDatasources).toHaveLength(3); | ||
| expect(testDatasources[0]).toEqual({ | ||
| index: 0, | ||
| templateName: 'Test', | ||
| startBlock: 1, | ||
| endBlock: undefined, | ||
| args: undefined, | ||
| }); | ||
| expect(testDatasources[1]).toEqual({ | ||
| index: 1, | ||
| templateName: 'Test', | ||
| startBlock: 2, | ||
| endBlock: undefined, | ||
| args: undefined, | ||
| }); | ||
| expect(testDatasources[2]).toEqual({ | ||
| index: 2, | ||
| templateName: 'Test', | ||
| startBlock: 3, | ||
| endBlock: undefined, | ||
| args: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it('excludes destroyed datasources from list', async () => { | ||
| const destroyedParam = {...testParam1, endBlock: 50}; | ||
| const meta = mockMetadata([destroyedParam, testParam2, testParam3]); | ||
| await service.init(meta); | ||
|
|
||
| const datasources = service.getDynamicDatasourcesByTemplate('Test'); | ||
|
|
||
| expect(datasources).toHaveLength(2); | ||
| expect(datasources[0].index).toBe(1); // Global index | ||
| expect(datasources[0].startBlock).toBe(2); | ||
| expect(datasources[1].index).toBe(2); // Global index | ||
| expect(datasources[1].startBlock).toBe(3); | ||
| }); | ||
|
|
||
| it('returns empty array when no datasources match template', async () => { | ||
| const meta = mockMetadata([testParamOther]); | ||
| await service.init(meta); | ||
|
|
||
| const datasources = service.getDynamicDatasourcesByTemplate('Test'); | ||
|
|
||
| expect(datasources).toEqual([]); | ||
| }); | ||
|
|
||
| it('includes args in datasource info when present', async () => { | ||
| const paramWithArgs = {...testParam1, args: {address: '0x123', tokenId: 1}}; | ||
| const meta = mockMetadata([paramWithArgs]); | ||
| await service.init(meta); | ||
|
|
||
| const datasources = service.getDynamicDatasourcesByTemplate('Test'); | ||
|
|
||
| expect(datasources).toHaveLength(1); | ||
| expect(datasources[0].args).toEqual({address: '0x123', tokenId: 1}); | ||
| }); | ||
|
|
||
| it('throws error when service not initialized', () => { | ||
| expect(() => service.getDynamicDatasourcesByTemplate('Test')).toThrow( | ||
| 'DynamicDsService has not been initialized' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('destroyDynamicDatasource with index', () => { | ||
| it('destroys specific datasource by index', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParam3, testParamOther]); | ||
| await service.init(meta); | ||
|
|
||
| await service.destroyDynamicDatasource('Test', 50, 1); | ||
|
|
||
| const updatedParams = (service as any)._datasourceParams; | ||
| expect(updatedParams[0]).toEqual(testParam1); // Not destroyed | ||
| expect(updatedParams[1]).toEqual({...testParam2, endBlock: 50}); // Destroyed | ||
| expect(updatedParams[2]).toEqual(testParam3); // Not destroyed | ||
| expect(updatedParams[3]).toEqual(testParamOther); // Not destroyed | ||
| }); | ||
|
|
||
| it('throws error when index is out of bounds', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2]); | ||
| await service.init(meta); | ||
|
|
||
| await expect(service.destroyDynamicDatasource('Test', 50, 5)).rejects.toThrow( | ||
| 'Index 5 is out of bounds. There are 2 datasource(s) in total' | ||
| ); | ||
| }); | ||
|
|
||
| it('throws error when index is negative', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2]); | ||
| await service.init(meta); | ||
|
|
||
| await expect(service.destroyDynamicDatasource('Test', 50, -1)).rejects.toThrow( | ||
| 'Index -1 is out of bounds. There are 2 datasource(s) in total' | ||
| ); | ||
| }); | ||
|
|
||
| it('throws error when trying to destroy already destroyed datasource', async () => { | ||
| const destroyedParam = {...testParam1, endBlock: 30}; | ||
| const meta = mockMetadata([destroyedParam]); | ||
| await service.init(meta); | ||
|
|
||
| await expect(service.destroyDynamicDatasource('Test', 50, 0)).rejects.toThrow( | ||
| 'Dynamic datasource at index 0 is already destroyed' | ||
| ); | ||
| }); | ||
|
|
||
| it('correctly handles global index after some datasources are destroyed', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParam3, testParam4]); | ||
| await service.init(meta); | ||
|
|
||
| // Destroy the first one using global index 0 | ||
| await service.destroyDynamicDatasource('Test', 40, 0); | ||
|
|
||
| // Now only 3 active datasources for 'Test' template, with global indices 1, 2, 3 | ||
| const activeDatasources = service.getDynamicDatasourcesByTemplate('Test'); | ||
| expect(activeDatasources).toHaveLength(3); | ||
| expect(activeDatasources[0].index).toBe(1); // Global index | ||
| expect(activeDatasources[0].startBlock).toBe(2); | ||
| expect(activeDatasources[1].index).toBe(2); // Global index | ||
| expect(activeDatasources[1].startBlock).toBe(3); | ||
| expect(activeDatasources[2].index).toBe(3); // Global index | ||
| expect(activeDatasources[2].startBlock).toBe(4); | ||
|
|
||
| // Destroy using global index 2 (testParam3) | ||
| await service.destroyDynamicDatasource('Test', 60, 2); | ||
|
|
||
| const updatedParams = (service as any)._datasourceParams; | ||
| expect(updatedParams[0]).toEqual({...testParam1, endBlock: 40}); | ||
| expect(updatedParams[1]).toEqual(testParam2); // Still active | ||
| expect(updatedParams[2]).toEqual({...testParam3, endBlock: 60}); | ||
| expect(updatedParams[3]).toEqual(testParam4); // Still active | ||
| }); | ||
|
|
||
| it('updates datasources in memory correctly when destroying by index', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParam3]); | ||
| await service.init(meta); | ||
|
|
||
| await service.destroyDynamicDatasource('Test', 100, 1); | ||
|
|
||
| const datasources = (service as any)._datasources; | ||
| expect(datasources[0].endBlock).toBeUndefined(); | ||
| expect(datasources[1].endBlock).toBe(100); | ||
| expect(datasources[2].endBlock).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('allows destroying datasources from different templates independently', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParamOther]); | ||
| await service.init(meta); | ||
|
|
||
| await service.destroyDynamicDatasource('Test', 50, 0); | ||
| await service.destroyDynamicDatasource('Other', 60, 2); | ||
|
|
||
| const updatedParams = (service as any)._datasourceParams; | ||
| expect(updatedParams[0]).toEqual({...testParam1, endBlock: 50}); | ||
| expect(updatedParams[1]).toEqual(testParam2); // Not destroyed | ||
| expect(updatedParams[2]).toEqual({...testParamOther, endBlock: 60}); | ||
| }); | ||
|
|
||
| it('throws error when template name does not match global index', async () => { | ||
| const meta = mockMetadata([testParam1, testParam2, testParamOther]); | ||
| await service.init(meta); | ||
|
|
||
| // Try to destroy 'Test' template with index 2, which is 'Other' template | ||
| await expect(service.destroyDynamicDatasource('Test', 50, 2)).rejects.toThrow( | ||
| 'Datasource at index 2 has template name "Other", not "Test"' | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
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:
🤖 Prompt for AI Agents