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, headers?: Record<string, string>) => Promise<unknown>;
/** Resolvers the RPC layer applies to enrich responses before consumers see them, keyed by concern. */
resolvers?: {
team?: (uuids: string[]) => Promise<Record<string, TeamInfo>>;
};
};

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 an error normalization function to convert transport-specific errors into ApplicationError. A normalizer returns null for errors it does not recognize, and the provider then falls back to normalizeUniversalError, which handles plain Error objects, strings, and other shapes. ErrorNormalizer is defined in types/error-types.ts:

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

export interface ErrorContextValue {
normalizeError: ErrorNormalizer;
}

Transcoder Error Normalization​

The UI talks to the API server with proto3 JSON over HTTP, through Envoy's gRPC JSON transcoder. The RPC package's fetch transport throws a GrpcTranscoderError (with code and details fields) for any non-OK response. It builds the error from the google.rpc.Status JSON body when present, falls back to the grpc-status and grpc-message response headers, and otherwise uses gRPC code 2 (UNKNOWN).

The RPC package provides normalizeTranscoderError to convert these into ApplicationError:

export const normalizeTranscoderError: ErrorNormalizer = (error: unknown) => {
if (!(error instanceof GrpcTranscoderError)) {
return null;
}

const code = (
error.code in GrpcStatusCode ? error.code : GrpcStatusCode.UNKNOWN
) as GrpcStatusCode;

return new ApplicationError(error.message, code, {
source: 'grpc-transcoder',
meta: { details: error.details },
cause: error,
});
};

The resulting ApplicationError has source: 'grpc-transcoder', a gRPC status code (unrecognized codes become UNKNOWN), the status details array in meta.details, and the original GrpcTranscoderError as cause. Any other error type returns null and is handled by normalizeUniversalError.

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'>
extends InterpolationContextExtensions,
UserDataSources {
repeatedLayoutContext?: RepeatedLayoutState;
studio: ViewTypeToParamType<U>; // URL parameters
data: any; // Resolved from row ?? page
}

// UserDataSources
// page: any; // Detail/form view data
// row: any; // Table row data
// initialValues: any; // Form initial state
// response: any; // Mutation response data
// mutationName?: string;

InterpolationContextExtensions is an empty interface that applications can augment through module declaration to add their own context.

Interpolatable Type​

Fields that accept interpolation use the Interpolatable type:

export type Interpolatable<T, U extends StudioParamsView = 'base'> =
| T
| string
| FunctionInterpolation<T, U>
| StringInterpolation<U>;

Config types usually get interpolation through DeepInterpolatable<T>, which makes every leaf of T interpolatable (functions and React components are left as-is). For example, actions are written as ActionConfigSchema, which is DeepInterpolatable<ActionConfig>, so a confirm modal's title can be computed from the record:

// From: config/entities/deployment/deployment.ts
// DeploymentRecord is the deployment entity's record type, defined in that file's package.
modal: {
type: 'confirm',
header: {
title: interpolate(
({ data }) => `Are you sure you want to retire ${(data as DeploymentRecord).metadata?.name}`
),
},
},

See the Action Configuration Reference.

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?, pipelineName?
formprojectId, phase, entity, entityId, entitySubType?, revisionId?
form-detailParameters shared by form and detail views; the rest are optional
baseAll available parameters with optional fields
unregisteredUntyped Record<string, string | undefined> of route and query params

Provider Composition​

The application passes its injected dependencies to CoreApp, which composes the providers in a fixed hierarchy:

// app/App.tsx
const dependencies = {
error: { normalizeError: normalizeTranscoderError },
theme: { icons: ICONS },
service: { request, resolvers: { team: resolveTeams } },
user: { name, email, role, timeZone },
};

<CoreApp dependencies={dependencies} />
// packages/core/index.tsx (simplified)
<ServiceProvider {...dependencies.service}>
<ErrorProvider {...dependencies.error}>
<IconProvider icons={dependencies.theme.icons}>
<UserProvider {...dependencies.user}>
{children}
</UserProvider>
</IconProvider>
</ErrorProvider>
</ServiceProvider>

InterpolationProvider is not part of this composition; it is mounted where interpolation context extensions are needed.

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/