chart data loading
This commit is contained in:
190
gateway/src/workspace/container-sync.ts
Normal file
190
gateway/src/workspace/container-sync.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Container Sync
|
||||
*
|
||||
* Handles synchronization of persistent workspace stores with the user container
|
||||
* via MCP tools. Persistent stores (chartStore, userPreferences, etc.) are
|
||||
* stored in the container and loaded/saved via MCP tool calls.
|
||||
*
|
||||
* Container-side storage: /data/workspace/{store_name}.json
|
||||
*
|
||||
* MCP Tools used:
|
||||
* - workspace_read(store_name) -> dict
|
||||
* - workspace_write(store_name, data) -> None
|
||||
* - workspace_patch(store_name, patch) -> dict (new state)
|
||||
*/
|
||||
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { Operation as JsonPatchOp } from 'fast-json-patch';
|
||||
import type { MCPClientConnector } from '../harness/mcp-client.js';
|
||||
|
||||
/**
|
||||
* Result of loading a store from the container.
|
||||
*/
|
||||
export interface LoadResult {
|
||||
exists: boolean;
|
||||
state?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of saving a store to the container.
|
||||
*/
|
||||
export interface SaveResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of patching a store in the container.
|
||||
*/
|
||||
export interface PatchResult {
|
||||
success: boolean;
|
||||
newState?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles synchronization with the user's container via MCP.
|
||||
*/
|
||||
export class ContainerSync {
|
||||
private mcpClient: MCPClientConnector;
|
||||
private logger: FastifyBaseLogger;
|
||||
|
||||
constructor(mcpClient: MCPClientConnector, logger: FastifyBaseLogger) {
|
||||
this.mcpClient = mcpClient;
|
||||
this.logger = logger.child({ component: 'ContainerSync' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a workspace store from the container.
|
||||
* Returns the stored state or indicates the store doesn't exist.
|
||||
*/
|
||||
async loadStore(storeName: string): Promise<LoadResult> {
|
||||
if (!this.mcpClient.isConnected()) {
|
||||
this.logger.warn({ store: storeName }, 'MCP client not connected, cannot load store');
|
||||
return { exists: false, error: 'MCP client not connected' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.debug({ store: storeName }, 'Loading store from container');
|
||||
|
||||
const result = (await this.mcpClient.callTool('workspace_read', {
|
||||
store_name: storeName,
|
||||
})) as { exists: boolean; data?: unknown; error?: string };
|
||||
|
||||
if (result.error) {
|
||||
this.logger.warn({ store: storeName, error: result.error }, 'Container returned error');
|
||||
return { exists: false, error: result.error };
|
||||
}
|
||||
|
||||
if (!result.exists) {
|
||||
this.logger.debug({ store: storeName }, 'Store does not exist in container');
|
||||
return { exists: false };
|
||||
}
|
||||
|
||||
this.logger.debug({ store: storeName }, 'Loaded store from container');
|
||||
return { exists: true, state: result.data };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.logger.error({ store: storeName, error: message }, 'Failed to load store from container');
|
||||
return { exists: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a workspace store to the container.
|
||||
* Overwrites any existing state.
|
||||
*/
|
||||
async saveStore(storeName: string, state: unknown): Promise<SaveResult> {
|
||||
if (!this.mcpClient.isConnected()) {
|
||||
this.logger.warn({ store: storeName }, 'MCP client not connected, cannot save store');
|
||||
return { success: false, error: 'MCP client not connected' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.debug({ store: storeName }, 'Saving store to container');
|
||||
|
||||
const result = (await this.mcpClient.callTool('workspace_write', {
|
||||
store_name: storeName,
|
||||
data: state,
|
||||
})) as { success: boolean; error?: string };
|
||||
|
||||
if (result.error || !result.success) {
|
||||
this.logger.warn({ store: storeName, error: result.error }, 'Failed to save store');
|
||||
return { success: false, error: result.error || 'Unknown error' };
|
||||
}
|
||||
|
||||
this.logger.debug({ store: storeName }, 'Saved store to container');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.logger.error({ store: storeName, error: message }, 'Failed to save store to container');
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a JSON patch to a store in the container.
|
||||
* Returns the new state after applying the patch.
|
||||
*/
|
||||
async patchStore(storeName: string, patch: JsonPatchOp[]): Promise<PatchResult> {
|
||||
if (!this.mcpClient.isConnected()) {
|
||||
this.logger.warn({ store: storeName }, 'MCP client not connected, cannot patch store');
|
||||
return { success: false, error: 'MCP client not connected' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.debug({ store: storeName, patchOps: patch.length }, 'Patching store in container');
|
||||
|
||||
const result = (await this.mcpClient.callTool('workspace_patch', {
|
||||
store_name: storeName,
|
||||
patch,
|
||||
})) as { success: boolean; data?: unknown; error?: string };
|
||||
|
||||
if (result.error || !result.success) {
|
||||
this.logger.warn({ store: storeName, error: result.error }, 'Failed to patch store');
|
||||
return { success: false, error: result.error || 'Unknown error' };
|
||||
}
|
||||
|
||||
this.logger.debug({ store: storeName }, 'Patched store in container');
|
||||
return { success: true, newState: result.data };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.logger.error({ store: storeName, error: message }, 'Failed to patch store in container');
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all persistent stores from the container.
|
||||
* Returns a map of store name -> state.
|
||||
*/
|
||||
async loadAllStores(storeNames: string[]): Promise<Map<string, unknown>> {
|
||||
const states = new Map<string, unknown>();
|
||||
|
||||
for (const storeName of storeNames) {
|
||||
const result = await this.loadStore(storeName);
|
||||
if (result.exists && result.state !== undefined) {
|
||||
states.set(storeName, result.state);
|
||||
}
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all persistent stores to the container.
|
||||
*/
|
||||
async saveAllStores(stores: Map<string, unknown>): Promise<void> {
|
||||
for (const [storeName, state] of stores) {
|
||||
await this.saveStore(storeName, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MCP client is connected.
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.mcpClient.isConnected();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user