Skip to main content

Core Systems

Service Provider

The Service Provider implements dependency injection for RPC requests. The core library doesn't know about specific backends - applications inject their own request function.

Context Definition

ServiceContextType:

export type ServiceContextType = {
request: (requestId: string, args: unknown) => Promise<unknown>;
};

Usage

The request function is injected by the application and called by hooks:

import { useServiceProvider } from '#core/providers/service-provider/use-service-provider';

function MyComponent() {
const { request } = useServiceProvider();

const handleFetch = async () => {
const result = await request('GetPipeline', { name: 'my-pipeline' });
};
}

Error Handling

ApplicationError

ApplicationError is the standard error class used throughout the application:

export class ApplicationError extends Error {
name = 'ApplicationError' as const;
code: number; // gRPC status code
meta?: Record<string, unknown>;
cause?: unknown;
source?: string; // Error source identifier
}

ErrorProvider

The ErrorProvider supplies error normalization functions to convert framework-specific errors into ApplicationError:

export type ErrorNormalizer = (error: unknown) => ApplicationError | null;

export interface ErrorContextValue {
normalizeError: ErrorNormalizer;
}

Connect RPC Error Normalization

The RPC package provides normalizeConnectError for Connect RPC errors:

import { ConnectError } from '@connectrpc/connect';

export const normalizeConnectError: ErrorNormalizer = (error) => {
if (!(error instanceof ConnectError)) {
return null;
}

return new ApplicationError(error.message, mapConnectCodeToGrpc(error.code), {
source: 'connect-rpc',
meta: {
connectErrorName: error.name,
details: error.details,
metadata: error.metadata,
},
cause: error.cause ?? error,
});
};

Interpolation System

Configuration objects can contain dynamic values that get resolved at runtime. Two forms are supported: string templates and functions.

String Interpolation

StringInterpolation replaces ${path.to.value} patterns with context values:

const interpolation = new StringInterpolation('Hello ${page.title}');
const result = interpolation.execute({ page: { title: 'Dashboard' } });
// result: "Hello Dashboard"

Function Interpolation

FunctionInterpolation executes functions with full context access:

const interpolation = new FunctionInterpolation(
({ page, row }) => `${page.title}: ${row.status}`
);
const result = interpolation.execute({
page: { title: 'Pipeline' },
row: { status: 'Running' }
});
// result: "Pipeline: Running"

Interpolation Context

InterpolationContext is available to interpolation functions:

export interface InterpolationContext<U extends StudioParamsView = 'base'> {
page: any; // Detail/form view data
row: any; // Table row data
initialValues: any; // Form initial state
response: any; // Mutation response data
data: any; // Resolved from row ?? page
studio: ViewTypeToParamType<U>; // URL parameters
repeatedLayoutContext?: RepeatedLayoutState;
}

Interpolatable Type

Fields that accept interpolation use the Interpolatable type:

export type Interpolatable<T> = T | string | FunctionInterpolation<T> | StringInterpolation;

// Usage in configuration
interface ActionConfig {
title: Interpolatable<string>;
disabled: Interpolatable<boolean>;
}

Hook Patterns

useStudioQuery

useStudioQuery fetches data with automatic error normalization and interpolation:

const { data, isLoading, error } = useStudioQuery<Pipeline>({
queryName: 'GetPipeline',
serviceOptions: { name: 'my-pipeline' },
clientOptions: { enabled: true },
});

// With interpolation
const { data } = useStudioQuery({
queryName: 'GetPipeline',
serviceOptions: { name: interpolate('${row.pipelineName}') },
});

The hook:

  • Resolves interpolated values in serviceOptions
  • Uses projectId as namespace by default
  • Normalizes errors to ApplicationError
  • Integrates with TanStack Query

useStudioMutation

useStudioMutation performs mutations with error normalization:

const { mutate, isPending, error } = useStudioMutation<Pipeline, CreatePipelineInput>({
mutationName: 'CreatePipeline',
clientOptions: {
onSuccess: (data) => { /* handle success */ },
onError: (error) => { /* handle error */ },
},
});

mutate({ name: 'new-pipeline', config: { /* ... */ } });

useStudioParams

useStudioParams extracts and transforms URL parameters based on view type:

// For list views
const { projectId, phase, entity } = useStudioParams('list');

// For detail views
const { projectId, phase, entity, entityId, entityTab } = useStudioParams('detail');

// For form views
const { projectId, phase, entity, entityId, entitySubType } = useStudioParams('form');

View types and their parameters:

View TypeParameters
listprojectId, phase, entity
detailprojectId, phase, entity, entityId, entityTab, revisionId?
formprojectId, phase, entity, entityId, entitySubType?, revisionId?
baseAll available parameters with optional fields

Provider Composition

Providers are composed in a specific hierarchy. The application typically wraps components with:

<ServiceProvider request={rpcRequest}>
<ErrorProvider normalizeError={normalizeConnectError}>
<InterpolationProvider>
<IconProvider icons={iconMap}>
<UserProvider user={currentUser}>
{children}
</UserProvider>
</IconProvider>
</InterpolationProvider>
</ErrorProvider>
</ServiceProvider>

Available providers:

ProviderPurposeSource
ServiceProviderRPC request injectionproviders/service-provider/
ErrorProviderError normalizationproviders/error-provider/
InterpolationProviderValue interpolation contextproviders/interpolation-provider/
IconProviderIcon component mappingproviders/icon-provider/
UserProviderCurrent user contextproviders/user-provider/
CellProviderCell rendering contextproviders/cell-provider/