diff --git a/package.json b/package.json index b65137df6..f98270ca0 100644 --- a/package.json +++ b/package.json @@ -286,6 +286,7 @@ "@vscode/extension-telemetry": "^0.7.7", "@vscode/vsce": "^2.22.0", "axios": "^1.2.2", + "axios-retry": "^4.0.0", "diff": "^5.1.0", "fast-deep-equal": "^3.1.3", "fast-glob": "^3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d44b081b8..089b9605b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: axios: specifier: ^1.2.2 version: 1.4.0 + axios-retry: + specifier: ^4.0.0 + version: 4.0.0(axios@1.4.0) diff: specifier: ^5.1.0 version: 5.1.0 @@ -1585,6 +1588,15 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /axios-retry@4.0.0(axios@1.4.0): + resolution: {integrity: sha512-F6P4HVGITD/v4z9Lw2mIA24IabTajvpDZmKa6zq/gGwn57wN5j1P3uWrAV0+diqnW6kTM2fTqmWNfgYWGmMuiA==} + peerDependencies: + axios: 0.x || 1.x + dependencies: + axios: 1.4.0 + is-retry-allowed: 2.2.0 + dev: false + /axios@1.4.0: resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} dependencies: @@ -2979,6 +2991,11 @@ packages: isobject: 3.0.1 dev: true + /is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + dev: false + /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} dev: true diff --git a/src/axios/index.ts b/src/axios/index.ts new file mode 100644 index 000000000..4b182a00f --- /dev/null +++ b/src/axios/index.ts @@ -0,0 +1,8 @@ +import axios from 'axios'; +import axiosRetry from 'axios-retry'; + +const client = axios.create(); +axiosRetry(client); + +export const DEFAULT_RETRY_COUNT = 3; +export default client; \ No newline at end of file diff --git a/src/components/bootstrapExecutablesService.ts b/src/components/bootstrapExecutablesService.ts index 359a54e22..2339bec6f 100644 --- a/src/components/bootstrapExecutablesService.ts +++ b/src/components/bootstrapExecutablesService.ts @@ -1,6 +1,7 @@ -import { FileSystem, Uri } from 'vscode'; +import { FileSystem, Uri, window } from 'vscode'; import { DownloadService, ForbiddenRequestError } from './downloadService'; import { MessageBus, MessageKind } from './messageBus'; +import { Telemetry } from '../telemetry/telemetry'; // aka bootstrap engines export class BootstrapExecutablesService { @@ -9,6 +10,7 @@ export class BootstrapExecutablesService { private readonly __globalStorageUri: Uri, private readonly __fileSystem: FileSystem, private readonly __messageBus: MessageBus, + private readonly __telemetryService: Telemetry ) { __messageBus.subscribe(MessageKind.bootstrapEngine, () => this.__onBootstrapEngines(), @@ -18,19 +20,31 @@ export class BootstrapExecutablesService { private async __onBootstrapEngines() { await this.__fileSystem.createDirectory(this.__globalStorageUri); - // Uri.file('/intuita/nora-node-engine/package/intuita-linux') - const codemodEngineNodeExecutableUri = - await this.__bootstrapCodemodEngineNodeExecutableUri(); + try { + // Uri.file('/intuita/nora-node-engine/package/intuita-linux') + const codemodEngineNodeExecutableUri = + await this.__bootstrapCodemodEngineNodeExecutableUri(); - // Uri.file('/intuita/codemod-engine-rust/target/release/codemod-engine-rust'); + // Uri.file('/intuita/codemod-engine-rust/target/release/codemod-engine-rust'); const codemodEngineRustExecutableUri = - await this.__bootstrapCodemodEngineRustExecutableUri(); + await this.__bootstrapCodemodEngineRustExecutableUri(); this.__messageBus.publish({ kind: MessageKind.engineBootstrapped, codemodEngineNodeExecutableUri, codemodEngineRustExecutableUri, }); + + } catch(e) { + const message = e instanceof Error ? e.message : String(e); + + window.showErrorMessage(message); + + this.__telemetryService.sendError({ + kind: 'failedToBootstrapEngines', + message, + }); + } } private async __bootstrapCodemodEngineNodeExecutableUri(): Promise { diff --git a/src/components/downloadService.ts b/src/components/downloadService.ts index 2e4a3213d..925b08d7d 100644 --- a/src/components/downloadService.ts +++ b/src/components/downloadService.ts @@ -1,4 +1,5 @@ -import axios from 'axios'; +import { isAxiosError } from 'axios'; +import axios, { DEFAULT_RETRY_COUNT } from '../axios'; import { Mode } from 'node:fs'; import { FileSystem, Uri } from 'vscode'; import { FileSystemUtilities } from './fileSystemUtilities'; @@ -29,13 +30,18 @@ export class DownloadService { let response; try { - response = await axios.head(url, { timeout: 5000 }); + response = await axios.head(url, { + timeout: 15000, + 'axios-retry': { + retries: DEFAULT_RETRY_COUNT + } + }); } catch (error) { if (localModificationTime > 0) { return false; } - if (!axios.isAxiosError(error)) { + if (!isAxiosError(error)) { throw error; } @@ -69,11 +75,16 @@ export class DownloadService { uri: Uri, chmod: Mode | null, ): Promise { - const response = await axios.get(url, { responseType: 'arraybuffer' }); + const response = await axios.get(url, { + responseType: 'arraybuffer', + 'axios-retry': { + retries: DEFAULT_RETRY_COUNT + } + }); const content = new Uint8Array(response.data); await this.#fileSystem.writeFile(uri, content); - + if (chmod !== null) { await this.#fileSystemUtilities.setChmod(uri, chmod); } diff --git a/src/data/index.ts b/src/data/index.ts index afdefe278..e32e6d2d3 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -6,6 +6,7 @@ import rootReducer, { actions, getInitialState } from './slice'; import { Memento } from 'vscode'; import { PersistPartial } from 'redux-persist/es/persistReducer'; import { persistedStateCodecNew } from '../persistedState/codecs'; +import { window } from 'vscode'; const PERSISTANCE_PREFIX = 'persist'; const PERSISTANCE_KEY = 'compressedRoot'; @@ -18,7 +19,7 @@ const deserializeState = (serializedState: string) => { const rawState = JSON.parse(serializedState); if (typeof rawState !== 'object' || rawState === null) { - return; + return null; } Object.entries(rawState).forEach(([key, value]) => { @@ -30,11 +31,36 @@ const deserializeState = (serializedState: string) => { }); } catch (e) { console.error(e); + + return null; } return parsedState; }; +const getPreloadedState = async (storage: MementoStorage) => { +const initialState = + (await storage.getItem(`${PERSISTANCE_PREFIX}:${PERSISTANCE_KEY}`)); + +if(initialState === null) { + return null; +} + +const deserializedState = deserializeState(initialState); + +if(deserializeState === null) { + return null; +} + +const decodedState = persistedStateCodecNew.decode(deserializedState); + +// should never happen because of codec fallback +if (decodedState._tag !== 'Right') { + throw new Error('Invalid state'); +} + +return decodedState.right; +} const buildStore = async (workspaceState: Memento) => { const storage = new MementoStorage(workspaceState); @@ -65,21 +91,15 @@ const buildStore = async (workspaceState: Memento) => { return persistedReducer(state, action); }; - const initialState = - (await storage.getItem(`${PERSISTANCE_PREFIX}:${PERSISTANCE_KEY}`)) ?? - ''; - const deserializedState = deserializeState(initialState); + const preloadedState = await getPreloadedState(storage); - const decodedState = persistedStateCodecNew.decode(deserializedState); - - // should never happen because of codec fallback - if (decodedState._tag !== 'Right') { - throw new Error('Invalid state'); + if(preloadedState === null) { + window.showWarningMessage('Unable to get preloaded state'); } - + const store = configureStore({ reducer: validatedReducer, - preloadedState: decodedState.right, + ...(preloadedState !== null && { preloadedState }), }); const persistor = persistStore(store); diff --git a/src/extension.ts b/src/extension.ts index 8964e8c6f..e6b1a061e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -109,6 +109,12 @@ export async function activate(context: vscode.ExtensionContext) { }), ); + const telemetryKey = 'd9f8ad27-50df-46e3-8acf-81ea279c8444'; + const vscodeTelemetry = new VscodeTelemetry( + new TelemetryReporter(telemetryKey), + messageBus, + ); + const fileService = new FileService(messageBus); const jobManager = new JobManager(fileService, messageBus, store); @@ -134,16 +140,13 @@ export async function activate(context: vscode.ExtensionContext) { context.globalStorageUri, vscode.workspace.fs, messageBus, + vscodeTelemetry ); const intuitaTextDocumentContentProvider = new IntuitaTextDocumentContentProvider(); - const telemetryKey = 'd9f8ad27-50df-46e3-8acf-81ea279c8444'; - const vscodeTelemetry = new VscodeTelemetry( - new TelemetryReporter(telemetryKey), - messageBus, - ); + const mainViewProvider = new MainViewProvider( context, diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 145c7eef8..5d80cbcb5 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -3,7 +3,11 @@ import type { CaseHash } from '../cases/types'; export type ErrorEvent = Readonly<{ kind: 'failedToExecuteCommand'; commandName: string; -}>; +}> | +Readonly<{ + kind: 'failedToBootstrapEngines', + message: string; +}> export type Event = | Readonly<{