Skip to content
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

fix(VSCode): Provide timeout and fallback for KaotoEditorFactory #1270

Merged
merged 1 commit into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions packages/ui/src/multiplying-architecture/KaotoEditorFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { EditorInitArgs, KogitoEditorEnvelopeContextType } from '@kie-tools-core/editor/dist/api';
import { ISettingsModel, NodeLabelType } from '../models';
import { KaotoEditorApp } from './KaotoEditorApp';
import { KaotoEditorChannelApi } from './KaotoEditorChannelApi';
import { KaotoEditorFactory } from './KaotoEditorFactory';
jest.mock('./KaotoEditorApp');

describe('KaotoEditorFactory', () => {
afterAll(() => {
jest.clearAllMocks();
});

it('should create editor', async () => {
const settingsModel: ISettingsModel = {
catalogUrl: 'catalog-url',
nodeLabel: NodeLabelType.Id,
};

const envelopeContext = {
channelApi: {
requests: {
getVSCodeKaotoSettings: () => Promise.resolve(settingsModel),
getCatalogURL: function (): Promise<string | undefined> {
throw new Error('Function not implemented.');
},
},
},
} as KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>;
const initArgs = {} as EditorInitArgs;
const factory = new KaotoEditorFactory();

const editor = await factory.createEditor(envelopeContext, initArgs);

expect(editor).toBeInstanceOf(KaotoEditorApp);
});

it('should get settings', async () => {
const settingsModel: ISettingsModel = {
catalogUrl: 'catalog-url',
nodeLabel: NodeLabelType.Id,
};

const getVSCodeKaotoSettingsSpy = jest.fn().mockResolvedValue(settingsModel);
const getCatalogURLSpy = jest.fn().mockRejectedValue(settingsModel);

const envelopeContext = {
channelApi: {
requests: {
getVSCodeKaotoSettings: getVSCodeKaotoSettingsSpy,
getCatalogURL: getCatalogURLSpy,
},
},
} as unknown as KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>;
const initArgs = {} as EditorInitArgs;
const factory = new KaotoEditorFactory();

const editor = await factory.createEditor(envelopeContext, initArgs);

expect(getVSCodeKaotoSettingsSpy).toHaveBeenCalledTimes(1);
expect(getCatalogURLSpy).not.toHaveBeenCalled();
expect(editor).toBeDefined();
});

it('should fallback to previous API if getVSCodeKaotoSettings is not implemented', async () => {
const getVSCodeKaotoSettingsSpy = jest.fn().mockImplementation(() => new Promise(() => {}));
const getCatalogURLSpy = jest.fn().mockResolvedValue('');

const envelopeContext = {
channelApi: {
requests: {
getVSCodeKaotoSettings: getVSCodeKaotoSettingsSpy,
getCatalogURL: getCatalogURLSpy,
},
},
} as unknown as KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>;
const initArgs = {} as EditorInitArgs;
const factory = new KaotoEditorFactory();

const editor = await factory.createEditor(envelopeContext, initArgs);

expect(getVSCodeKaotoSettingsSpy).toHaveBeenCalledTimes(1);
expect(getCatalogURLSpy).toHaveBeenCalledTimes(1);
expect(editor).toBeDefined();
});

it('should update catalog URL', async () => {
const settingsModel: ISettingsModel = {
catalogUrl: '',
nodeLabel: NodeLabelType.Id,
};

const getVSCodeKaotoSettingsSpy = jest.fn().mockResolvedValue(settingsModel);
const getCatalogURLSpy = jest.fn().mockRejectedValue(settingsModel);

const envelopeContext = {
channelApi: {
requests: {
getVSCodeKaotoSettings: getVSCodeKaotoSettingsSpy,
getCatalogURL: getCatalogURLSpy,
},
},
} as unknown as KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>;
const initArgs = {
resourcesPathPrefix: 'path-prefix',
} as EditorInitArgs;
const factory = new KaotoEditorFactory();

const editor = await factory.createEditor(envelopeContext, initArgs);

expect(KaotoEditorApp).toHaveBeenCalledWith(
envelopeContext,
initArgs,
expect.objectContaining({
settings: {
catalogUrl: 'path-prefix/camel-catalog/index.json',
nodeLabel: NodeLabelType.Id,
},
}),
);
expect(editor).toBeDefined();
});
});
33 changes: 30 additions & 3 deletions packages/ui/src/multiplying-architecture/KaotoEditorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
EditorInitArgs,
KogitoEditorEnvelopeContextType,
} from '@kie-tools-core/editor/dist/api';
import { DefaultSettingsAdapter } from '../models';
import { CatalogSchemaLoader, isDefined } from '../utils';
import { DefaultSettingsAdapter, ISettingsModel, SettingsModel } from '../models';
import { CatalogSchemaLoader, isDefined, promiseTimeout } from '../utils';
import { KaotoEditorApp } from './KaotoEditorApp';
import { KaotoEditorChannelApi } from './KaotoEditorChannelApi';

Expand All @@ -14,13 +14,40 @@ export class KaotoEditorFactory implements EditorFactory<Editor, KaotoEditorChan
envelopeContext: KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>,
initArgs: EditorInitArgs,
): Promise<Editor> {
const settings = await envelopeContext.channelApi.requests.getVSCodeKaotoSettings();
const settings = await this.getSettings(envelopeContext);

const settingsAdapter = new DefaultSettingsAdapter(settings);
this.updateCatalogUrl(settingsAdapter, initArgs);

return Promise.resolve(new KaotoEditorApp(envelopeContext, initArgs, settingsAdapter));
}

/**
* Get the settings from the envelope context
*/
private async getSettings(
envelopeContext: KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>,
): Promise<ISettingsModel> {
let settings: ISettingsModel;

try {
/**
* For non-implemented API methods, the promises won't resolve, for that reason
* we use a timeout to reject the promise and fallback to the previous API
*/
settings = await promiseTimeout(envelopeContext.channelApi.requests.getVSCodeKaotoSettings(), 500);
} catch (error) {
/**
* Reaching this point means that the new API is not available yet,
* so we fallback to the previous API
*/
const catalogUrl = await envelopeContext.channelApi.requests.getCatalogURL();
settings = new SettingsModel({ catalogUrl });
}

return settings;
}

/**
* Updates the catalog URL in the settings if it is not defined, to include the embedded catalog.
* It uses the resourcesPathPrefix from the initArgs to build the default catalog URL.
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './set-value';
export * from './get-custom-schema-from-kamelet';
export * from './update-kamelet-from-custom-schema';
export * from './pipe-custom-schema';
export * from './promise-timeout';
export * from './get-field-groups';
export * from './get-serialized-model';
export * from './get-user-updated-properties-schema';
Expand Down
54 changes: 54 additions & 0 deletions packages/ui/src/utils/promise-timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { promiseTimeout } from './promise-timeout';

describe('promiseTimeout', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('should resolve the promise if it resolves before the timeout', async () => {
const promise = Promise.resolve('foo');
const result = await promiseTimeout(promise, 1_000);

expect(result).toBe('foo');
});

it('should reject the promise if it rejects before the timeout', async () => {
const promise = Promise.reject(new Error('bar'));

await expect(promiseTimeout(promise, 1_000)).rejects.toThrow('bar');
});

it('should resolve the promise with the defaultValue when provided, if it takes longer than the timeout', async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('baz');
}, 1_000);
});

const promiseTimeoutResult = promiseTimeout(promise, 500, 'Lighting fast');

jest.advanceTimersByTime(500);

const result = await promiseTimeoutResult;

expect(result).toBe('Lighting fast');
});

it('should reject the promise if it takes longer than the timeout', async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('baz');
}, 1_000);
});

const promiseTimeoutResult = promiseTimeout(promise, 500);

jest.advanceTimersByTime(500);

await expect(promiseTimeoutResult).rejects.toThrow('Promise timed out');
});
});
19 changes: 19 additions & 0 deletions packages/ui/src/utils/promise-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const promiseTimeout: <T>(promise: Promise<T>, timeout: number, defaultValue?: T) => Promise<T> = <T>(
promise: Promise<T>,
timeout: number,
defaultValue?: T,
) => {
let timer: number;

const timeoutPromise = new Promise<T>((resolve, reject) => {
timer = setTimeout(() => {
if (defaultValue !== undefined) resolve(defaultValue);

reject(new Error('Promise timed out'));
}, timeout) as unknown as number;
});

return Promise.race([promise, timeoutPromise]).finally(() => {
clearTimeout(timer);
});
};
Loading