Skip to content

Commit

Permalink
send application tasks to frontend when updated (#102)
Browse files Browse the repository at this point in the history
* send application tasks to frontend when updated

* Models page also uses the store

* fix import
  • Loading branch information
feloy authored Jan 22, 2024
1 parent fdc085b commit 4696e3f
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 70 deletions.
6 changes: 5 additions & 1 deletion packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,15 @@ export class ApplicationManager {
}
}

let previousProgressValue = -1;
resp.on('data', chunk => {
progress += chunk.length;
const progressValue = (progress * 100) / totalFileSize;

taskUtil.setTaskProgress(modelId, progressValue);
if (progressValue === 100 || progressValue - previousProgressValue > 1) {
previousProgressValue = progressValue;
taskUtil.setTaskProgress(modelId, progressValue);
}

// send progress in percentage (ex. 1.2%, 2.6%, 80.1%) to frontend
//this.sendProgress(progressValue);
Expand Down
21 changes: 20 additions & 1 deletion packages/backend/src/registries/RecipeStatusRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,40 @@

import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import type { TaskRegistry } from './TaskRegistry';
import type { Webview } from '@podman-desktop/api';
import { MSG_NEW_RECIPE_STATE } from '@shared/Messages';

export class RecipeStatusRegistry {
private statuses: Map<string, RecipeStatus> = new Map<string, RecipeStatus>();

constructor(private taskRegistry: TaskRegistry) {}
constructor(
private taskRegistry: TaskRegistry,
private webview: Webview,
) {}

setStatus(recipeId: string, status: RecipeStatus) {
// Update the TaskRegistry
if (status.tasks && status.tasks.length > 0) {
status.tasks.map(task => this.taskRegistry.set(task));
}
this.statuses.set(recipeId, status);
this.dispatchState().catch((err: unknown) => {
console.error('error dispatching recipe statuses', err);
}); // we don't want to wait
}

getStatus(recipeId: string): RecipeStatus | undefined {
return this.statuses.get(recipeId);
}

getStatuses(): Map<string, RecipeStatus> {
return this.statuses;
}

private async dispatchState() {
await this.webview.postMessage({
id: MSG_NEW_RECIPE_STATE,
body: this.statuses,
});
}
}
4 changes: 0 additions & 4 deletions packages/backend/src/registries/TaskRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ import type { Task } from '@shared/src/models/ITask';
export class TaskRegistry {
private tasks: Map<string, Task> = new Map<string, Task>();

getTasksByLabel(label: string): Task[] {
return Array.from(this.tasks.values()).filter(task => label in (task.labels || {}));
}

set(task: Task) {
this.tasks.set(task.id, task);
}
Expand Down
2 changes: 0 additions & 2 deletions packages/backend/src/studio-api-impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import type { ApplicationManager } from './managers/applicationManager';
import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry';
import { StudioApiImpl } from './studio-api-impl';
import type { PlayGroundManager } from './managers/playground';
import type { TaskRegistry } from './registries/TaskRegistry';
import type { Webview } from '@podman-desktop/api';

import * as fs from 'node:fs';
Expand Down Expand Up @@ -75,7 +74,6 @@ beforeEach(async () => {
appUserDirectory,
} as unknown as ApplicationManager,
{} as unknown as RecipeStatusRegistry,
{} as unknown as TaskRegistry,
{} as unknown as PlayGroundManager,
catalogManager,
);
Expand Down
11 changes: 4 additions & 7 deletions packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import type { ApplicationManager } from './managers/applicationManager';
import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry';
import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import type { TaskRegistry } from './registries/TaskRegistry';
import type { Task } from '@shared/src/models/ITask';
import type { PlayGroundManager } from './managers/playground';
import * as podmanDesktopApi from '@podman-desktop/api';
import type { QueryState } from '@shared/src/models/IPlaygroundQueryState';
Expand All @@ -36,7 +34,6 @@ export class StudioApiImpl implements StudioAPI {
constructor(
private applicationManager: ApplicationManager,
private recipeStatusRegistry: RecipeStatusRegistry,
private taskRegistry: TaskRegistry,
private playgroundManager: PlayGroundManager,
private catalogManager: CatalogManager,
) {}
Expand All @@ -53,6 +50,10 @@ export class StudioApiImpl implements StudioAPI {
return this.recipeStatusRegistry.getStatus(recipeId);
}

async getPullingStatuses(): Promise<Map<string, RecipeStatus>> {
return this.recipeStatusRegistry.getStatuses();
}

async getModelById(modelId: string): Promise<ModelInfo> {
// TODO: move logic to catalog manager
const model = this.catalogManager.getModels().find(m => modelId === m.id);
Expand Down Expand Up @@ -83,10 +84,6 @@ export class StudioApiImpl implements StudioAPI {
return this.catalogManager.getModels().filter(m => localIds.includes(m.id));
}

async getTasksByLabel(label: string): Promise<Task[]> {
return this.taskRegistry.getTasksByLabel(label);
}

async startPlayground(modelId: string): Promise<void> {
// TODO: improve the following
const localModelInfo = this.applicationManager.getLocalModels().filter(m => m.id === modelId);
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/src/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class Studio {
this.rpcExtension = new RpcExtension(this.#panel.webview);
const gitManager = new GitManager();
const taskRegistry = new TaskRegistry();
const recipeStatusRegistry = new RecipeStatusRegistry(taskRegistry);
const recipeStatusRegistry = new RecipeStatusRegistry(taskRegistry, this.#panel.webview);
const applicationManager = new ApplicationManager(gitManager, recipeStatusRegistry, this.#extensionContext);
this.playgroundManager = new PlayGroundManager(this.#panel.webview);
// Create catalog manager, responsible for loading the catalog files and watching for changes
Expand All @@ -102,7 +102,6 @@ export class Studio {
this.studioApi = new StudioApiImpl(
applicationManager,
recipeStatusRegistry,
taskRegistry,
this.playgroundManager,
this.catalogManager,
);
Expand Down
30 changes: 11 additions & 19 deletions packages/frontend/src/pages/Models.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@ import ModelColumnRegistry from '../lib/table/model/ModelColumnRegistry.svelte';
import ModelColumnPopularity from '../lib/table/model/ModelColumnPopularity.svelte';
import ModelColumnLicense from '../lib/table/model/ModelColumnLicense.svelte';
import ModelColumnHw from '../lib/table/model/ModelColumnHW.svelte';
import { onDestroy, onMount } from 'svelte';
import { studioClient } from '/@/utils/client';
import type { Category } from '@shared/models/ICategory';
import type { Task } from '@shared/models/ITask';
import type { Task } from '@shared/src/models/ITask';
import TasksProgress from '/@/lib/progress/TasksProgress.svelte';
import { faRefresh } from '@fortawesome/free-solid-svg-icons';
import Card from '/@/lib/Card.svelte';
import Button from '/@/lib/button/Button.svelte';
import LinearProgress from '/@/lib/progress/LinearProgress.svelte';
import { modelsPulling } from '../stores/recipe';
import { onMount } from 'svelte';
const columns: Column<ModelInfo>[] = [
new Column<ModelInfo>('Name', { width: '4fr', renderer: ModelColumnName }),
Expand All @@ -29,7 +25,6 @@ const columns: Column<ModelInfo>[] = [
const row = new Row<ModelInfo>({});
let loading: boolean = true;
let intervalId: ReturnType<typeof setInterval> | undefined = undefined;
let tasks: Task[] = [];
let models: ModelInfo[] = [];
Expand All @@ -46,31 +41,28 @@ function filterModels(): void {
}
return previousValue;
}, [] as string[]);
filteredModels = models.filter((model) => !(model.id in modelsId));
filteredModels = models.filter((model) => !modelsId.includes(model.id));
}
onMount(() => {
// Pulling update
intervalId = setInterval(async () => {
tasks = await studioClient.getTasksByLabel("model-pulling");
const modelsPullingUnsubscribe = modelsPulling.subscribe(runningTasks => {
tasks = runningTasks;
loading = false;
filterModels();
}, 1000);
});
// Subscribe to the models store
return localModels.subscribe((value) => {
const localModelsUnsubscribe = localModels.subscribe((value) => {
models = value;
filterModels();
})
});
onDestroy(() => {
if(intervalId !== undefined) {
clearInterval(intervalId);
intervalId = undefined;
return () => {
modelsPullingUnsubscribe();
localModelsUnsubscribe();
}
});
</script>

<NavPage title="Models on disk" searchEnabled="{false}" loading="{loading}">
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/pages/Recipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import Recipe from './Recipe.svelte';
const mocks = vi.hoisted(() => {
return {
getCatalogMock: vi.fn(),
getPullingStatusesMock: vi.fn(),
};
});

vi.mock('../utils/client', async () => {
return {
studioClient: {
getCatalog: mocks.getCatalogMock,
getPullingStatuses: mocks.getPullingStatusesMock,
},
rpcBrowser: {
subscribe: () => {
Expand All @@ -29,6 +31,7 @@ test('should display recipe information', async () => {
expect(recipe).not.toBeUndefined();

mocks.getCatalogMock.mockResolvedValue(catalog);
mocks.getPullingStatusesMock.mockResolvedValue(new Map());
render(Recipe, {
recipeId: 'recipe 1',
});
Expand Down
28 changes: 3 additions & 25 deletions packages/frontend/src/pages/Recipe.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import NavPage from '/@/lib/NavPage.svelte';
import { onDestroy, onMount } from 'svelte';
import { studioClient } from '/@/utils/client';
import Tab from '/@/lib/Tab.svelte';
import Route from '/@/Route.svelte';
Expand All @@ -12,45 +11,24 @@ import { faDownload, faRefresh } from '@fortawesome/free-solid-svg-icons';
import TasksProgress from '/@/lib/progress/TasksProgress.svelte';
import Button from '/@/lib/button/Button.svelte';
import { getDisplayName } from '/@/utils/versionControlUtils';
import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import { getIcon } from '/@/utils/categoriesUtils';
import RecipeModels from './RecipeModels.svelte';
import { catalog } from '/@/stores/catalog';
import { recipes } from '/@/stores/recipe';
export let recipeId: string;
// The recipe model provided
$: recipe = $catalog.recipes.find(r => r.id === recipeId);
$: categories = $catalog.categories;
$: recipeStatus = $recipes.get(recipeId);
// By default, we are loading the recipe information
let loading: boolean = true;
// The pulling tasks
let recipeStatus: RecipeStatus | undefined = undefined;
let intervalId: ReturnType<typeof setInterval> | undefined = undefined;
onMount(async () => {
// Pulling update
intervalId = setInterval(async () => {
recipeStatus = await studioClient.getPullingStatus(recipeId);
loading = false;
}, 1000);
})
let loading: boolean = false;
const onPullingRequest = async () => {
loading = true;
await studioClient.pullApplication(recipeId);
}
onDestroy(() => {
if(intervalId !== undefined) {
clearInterval(intervalId);
intervalId = undefined;
}
});
const onClickRepository = () => {
if (recipe) {
studioClient.openURL(recipe.repository);
Expand Down
27 changes: 27 additions & 0 deletions packages/frontend/src/stores/recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Readable } from 'svelte/store';
import { derived, readable } from 'svelte/store';
import { MSG_NEW_RECIPE_STATE } from '@shared/Messages';
import { rpcBrowser, studioClient } from '/@/utils/client';
import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';

export const recipes: Readable<Map<string, RecipeStatus>> = readable<Map<string, RecipeStatus>>(
new Map<string, RecipeStatus>(),
set => {
const sub = rpcBrowser.subscribe(MSG_NEW_RECIPE_STATE, msg => {
set(msg);
});
// Initialize the store manually
studioClient.getPullingStatuses().then(state => {
set(state);
});
return () => {
sub.unsubscribe();
};
},
);

export const modelsPulling = derived(recipes, $recipes => {
return Array.from($recipes.values())
.flatMap(recipe => recipe.tasks)
.filter(task => 'model-pulling' in (task.labels || {}));
});
1 change: 1 addition & 0 deletions packages/shared/Messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const MSG_PLAYGROUNDS_STATE_UPDATE = 'playgrounds-state-update';
export const MSG_NEW_PLAYGROUND_QUERIES_STATE = 'new-playground-queries-state';
export const MSG_NEW_CATALOG_STATE = 'new-catalog-state';
export const MSG_NEW_RECIPE_STATE = 'new-recipe-state';
10 changes: 1 addition & 9 deletions packages/shared/src/StudioAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { RecipeStatus } from './models/IRecipeStatus';
import type { ModelInfo } from './models/IModelInfo';
import type { Task } from './models/ITask';
import type { QueryState } from './models/IPlaygroundQueryState';
import type { Catalog } from './models/ICatalog';
import type { PlaygroundState } from './models/IPlaygroundState';
Expand All @@ -9,6 +8,7 @@ export abstract class StudioAPI {
abstract ping(): Promise<string>;
abstract getCatalog(): Promise<Catalog>;
abstract getPullingStatus(recipeId: string): Promise<RecipeStatus>;
abstract getPullingStatuses(): Promise<Map<string, RecipeStatus>>;
abstract pullApplication(recipeId: string): Promise<void>;
abstract openURL(url: string): Promise<boolean>;
/**
Expand All @@ -19,14 +19,6 @@ export abstract class StudioAPI {
abstract startPlayground(modelId: string): Promise<void>;
abstract stopPlayground(modelId: string): Promise<void>;
abstract askPlayground(modelId: string, prompt: string): Promise<number>;

/**
* Get task by label
* @param label
*/
abstract getTasksByLabel(label: string): Promise<Task[]>;

abstract getPlaygroundQueriesState(): Promise<QueryState[]>;

abstract getPlaygroundsState(): Promise<PlaygroundState[]>;
}

0 comments on commit 4696e3f

Please sign in to comment.