@michelangelo-ai/rpc 0.3.0-nightly.20260701
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/handlers.test.ts +50 -0
- package/__tests__/request.test.ts +52 -0
- package/__tests__/runtime-config.test.ts +91 -0
- package/__tests__/services.test.ts +47 -0
- package/handlers.ts +68 -0
- package/index.ts +34 -0
- package/normalize-connect-error.ts +82 -0
- package/package.json +27 -0
- package/request.ts +45 -0
- package/runtime-config.ts +44 -0
- package/services.ts +62 -0
- package/transformations/common.ts +40 -0
- package/transformations/guards.ts +20 -0
- package/transformations/types.ts +123 -0
- package/tsconfig.json +14 -0
- package/types.ts +84 -0
- package/vite.config.ts +25 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getRpcHandlers } from '../handlers';
|
|
4
|
+
|
|
5
|
+
const mockGetServices = vi.hoisted(() => vi.fn());
|
|
6
|
+
vi.mock('../services', () => ({ getServices: mockGetServices }));
|
|
7
|
+
|
|
8
|
+
const createPipelineRun = vi.fn().mockResolvedValue({});
|
|
9
|
+
const updatePipelineRun = vi.fn().mockResolvedValue({});
|
|
10
|
+
const updateTriggerRun = vi.fn().mockResolvedValue({});
|
|
11
|
+
|
|
12
|
+
mockGetServices.mockResolvedValue(
|
|
13
|
+
new Proxy({} as Record<string, Record<string, unknown>>, {
|
|
14
|
+
get(_, serviceName: string) {
|
|
15
|
+
if (serviceName === 'PipelineRunService') {
|
|
16
|
+
return { createPipelineRun, updatePipelineRun };
|
|
17
|
+
}
|
|
18
|
+
if (serviceName === 'TriggerRunService') {
|
|
19
|
+
return { updateTriggerRun };
|
|
20
|
+
}
|
|
21
|
+
return new Proxy({}, { get: () => vi.fn().mockResolvedValue({}) });
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
describe('rpc handlers — mutation envelope wrapping', () => {
|
|
27
|
+
it('CreatePipelineRun wraps the bare record in a pipelineRun envelope', async () => {
|
|
28
|
+
const handlers = await getRpcHandlers();
|
|
29
|
+
const record = { metadata: { name: 'test-run' } };
|
|
30
|
+
await handlers.CreatePipelineRun(record as never);
|
|
31
|
+
|
|
32
|
+
expect(createPipelineRun).toHaveBeenCalledWith({ pipelineRun: record });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('UpdatePipelineRun wraps the bare record in a pipelineRun envelope', async () => {
|
|
36
|
+
const handlers = await getRpcHandlers();
|
|
37
|
+
const record = { metadata: { name: 'updated-run' } };
|
|
38
|
+
await handlers.UpdatePipelineRun(record as never);
|
|
39
|
+
|
|
40
|
+
expect(updatePipelineRun).toHaveBeenCalledWith({ pipelineRun: record });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('UpdateTriggerRun wraps the bare record in a triggerRun envelope', async () => {
|
|
44
|
+
const handlers = await getRpcHandlers();
|
|
45
|
+
const record = { metadata: { name: 'kill-target' } };
|
|
46
|
+
await handlers.UpdateTriggerRun(record as never);
|
|
47
|
+
|
|
48
|
+
expect(updateTriggerRun).toHaveBeenCalledWith({ triggerRun: record });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { request } from '../request';
|
|
4
|
+
|
|
5
|
+
vi.mock('../handlers', () => ({
|
|
6
|
+
getRpcHandlers: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const { getRpcHandlers } = await import('../handlers');
|
|
10
|
+
const mockGetRpcHandlers = getRpcHandlers as ReturnType<typeof vi.fn>;
|
|
11
|
+
|
|
12
|
+
function mockHandler(response: unknown) {
|
|
13
|
+
mockGetRpcHandlers.mockResolvedValue({
|
|
14
|
+
GetPipelineRun: vi.fn().mockResolvedValue(response),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it('strips $typeName and $unknown from response', async () => {
|
|
19
|
+
mockHandler({ $typeName: 'foo.Bar', $unknown: [], name: 'test' });
|
|
20
|
+
|
|
21
|
+
expect(await request('GetPipelineRun', {} as never)).toEqual({ name: 'test' });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('recursively strips protobuf internals from nested objects', async () => {
|
|
25
|
+
mockHandler({
|
|
26
|
+
$typeName: 'outer',
|
|
27
|
+
nested: { $typeName: 'inner', value: 1 },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(await request('GetPipelineRun', {} as never)).toEqual({ nested: { value: 1 } });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('preserves Uint8Array fields without corrupting them into plain objects', async () => {
|
|
34
|
+
const bytes = new Uint8Array([1, 2, 3]);
|
|
35
|
+
mockHandler({ $typeName: 'foo.Any', typeUrl: 'type.googleapis.com/foo', value: bytes });
|
|
36
|
+
|
|
37
|
+
const result = await request('GetPipelineRun', {} as never);
|
|
38
|
+
|
|
39
|
+
expect((result as { value: Uint8Array }).value).toBeInstanceOf(Uint8Array);
|
|
40
|
+
expect((result as { value: Uint8Array }).value).toEqual(bytes);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles arrays containing objects with protobuf internals', async () => {
|
|
44
|
+
mockHandler({
|
|
45
|
+
items: [
|
|
46
|
+
{ $typeName: 'foo', x: 1 },
|
|
47
|
+
{ $typeName: 'bar', x: 2 },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(await request('GetPipelineRun', {} as never)).toEqual({ items: [{ x: 1 }, { x: 2 }] });
|
|
52
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getRuntimeConfig } from '../runtime-config';
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
global.fetch = vi.fn();
|
|
7
|
+
const mockFetch = fetch as ReturnType<typeof vi.fn>;
|
|
8
|
+
|
|
9
|
+
// Mock window.location
|
|
10
|
+
Object.defineProperty(global, 'window', {
|
|
11
|
+
value: { location: { hostname: 'localhost' } },
|
|
12
|
+
writable: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('getApiConfig', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockFetch.mockClear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return apiBaseUrl from config.json when fetch succeeds', async () => {
|
|
21
|
+
mockFetch.mockResolvedValueOnce({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: () => Promise.resolve({ apiBaseUrl: 'http://production-envoy:8081' }),
|
|
24
|
+
} as Response);
|
|
25
|
+
|
|
26
|
+
const result = await getRuntimeConfig();
|
|
27
|
+
|
|
28
|
+
expect(result.apiBaseUrl).toBe('http://production-envoy:8081');
|
|
29
|
+
expect(mockFetch).toHaveBeenCalledWith('/config.json');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should throw error when fetch fails with network error', async () => {
|
|
33
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
34
|
+
|
|
35
|
+
await expect(getRuntimeConfig()).rejects.toThrow(
|
|
36
|
+
'Failed to load runtime configuration. Check that config.json is properly mounted.'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should throw error when config.json returns 404', async () => {
|
|
41
|
+
mockFetch.mockResolvedValueOnce({
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 404,
|
|
44
|
+
} as Response);
|
|
45
|
+
|
|
46
|
+
await expect(getRuntimeConfig()).rejects.toThrow(
|
|
47
|
+
'Failed to load runtime configuration. Check that config.json is properly mounted.'
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should throw error when config.json returns 500', async () => {
|
|
52
|
+
mockFetch.mockResolvedValueOnce({
|
|
53
|
+
ok: false,
|
|
54
|
+
status: 500,
|
|
55
|
+
} as Response);
|
|
56
|
+
|
|
57
|
+
await expect(getRuntimeConfig()).rejects.toThrow(
|
|
58
|
+
'Failed to load runtime configuration. Check that config.json is properly mounted.'
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should throw specific error when JSON parsing fails', async () => {
|
|
63
|
+
mockFetch.mockResolvedValueOnce({
|
|
64
|
+
ok: true,
|
|
65
|
+
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
66
|
+
} as Response);
|
|
67
|
+
|
|
68
|
+
await expect(getRuntimeConfig()).rejects.toThrow(
|
|
69
|
+
'Failed to load runtime configuration. Check that config.json contains valid JSON.'
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should throw specific error when apiBaseUrl field is missing', async () => {
|
|
74
|
+
mockFetch.mockResolvedValueOnce({
|
|
75
|
+
ok: true,
|
|
76
|
+
json: () => Promise.resolve({ someOtherField: 'value' }),
|
|
77
|
+
} as Response);
|
|
78
|
+
|
|
79
|
+
await expect(getRuntimeConfig()).rejects.toThrow(
|
|
80
|
+
'Failed to load runtime configuration. Check that config.json contains apiBaseUrl field.'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should throw specific error for network connectivity issues', async () => {
|
|
85
|
+
mockFetch.mockRejectedValueOnce(new TypeError('Failed to fetch'));
|
|
86
|
+
|
|
87
|
+
await expect(getRuntimeConfig()).rejects.toThrow(
|
|
88
|
+
'Failed to load runtime configuration. Check network connectivity.'
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { request } from '../request';
|
|
4
|
+
|
|
5
|
+
// Bypass the /config.json fetch — we only care about the RPC transport layer.
|
|
6
|
+
vi.mock('../runtime-config', () => ({
|
|
7
|
+
getRuntimeConfig: () => Promise.resolve({ apiBaseUrl: 'http://test' }),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// The real createConnectTransport (JSON mode) calls response.json() and decodes
|
|
11
|
+
// it with fromJson(..., jsonOptions). If jsonOptions.registry doesn't include
|
|
12
|
+
// TypedStructSchema, fromJson throws on the @type URL — so removing the registry
|
|
13
|
+
// from services.ts breaks this test.
|
|
14
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
17
|
+
json: () =>
|
|
18
|
+
Promise.resolve({
|
|
19
|
+
pipelineRunList: {
|
|
20
|
+
items: [
|
|
21
|
+
{
|
|
22
|
+
status: {
|
|
23
|
+
details: [
|
|
24
|
+
{
|
|
25
|
+
'@type': 'type.googleapis.com/michelangelo.api.TypedStruct',
|
|
26
|
+
typeUrl: 'type.googleapis.com/michelangelo.UniFlowConf',
|
|
27
|
+
value: {},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('decodes a ListPipelineRun response containing a TypedStruct Any field', async () => {
|
|
38
|
+
const result = await request('ListPipelineRun', {} as never);
|
|
39
|
+
const details = (
|
|
40
|
+
result as unknown as { pipelineRunList: { items: { status: { details: unknown[] } }[] } }
|
|
41
|
+
).pipelineRunList.items[0].status.details;
|
|
42
|
+
|
|
43
|
+
// The Any is decoded to { typeUrl, value: Uint8Array } by the registry.
|
|
44
|
+
// Without TypedStructSchema in the registry, fromJson throws before reaching here.
|
|
45
|
+
expect(details[0]).toMatchObject({ typeUrl: 'type.googleapis.com/michelangelo.api.TypedStruct' });
|
|
46
|
+
expect((details[0] as { value: unknown }).value).toBeInstanceOf(Uint8Array);
|
|
47
|
+
});
|
package/handlers.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getServices } from './services';
|
|
2
|
+
|
|
3
|
+
import type { PipelineRun } from './gen/michelangelo/api/v2/pipeline_run_pb';
|
|
4
|
+
import type { TriggerRun } from './gen/michelangelo/api/v2/trigger_run_pb';
|
|
5
|
+
import type { ExtractUnaryRpc } from './types';
|
|
6
|
+
|
|
7
|
+
let handlersPromise: Promise<Awaited<ReturnType<typeof createHandlers>>> | null = null;
|
|
8
|
+
|
|
9
|
+
async function createHandlers() {
|
|
10
|
+
const services = await getServices();
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
ListDeployment: services.DeploymentService.listDeployment as ExtractUnaryRpc<
|
|
14
|
+
typeof services.DeploymentService.listDeployment
|
|
15
|
+
>,
|
|
16
|
+
GetDeployment: services.DeploymentService.getDeployment as ExtractUnaryRpc<
|
|
17
|
+
typeof services.DeploymentService.getDeployment
|
|
18
|
+
>,
|
|
19
|
+
ListInferenceServer: services.InferenceServerService.listInferenceServer as ExtractUnaryRpc<
|
|
20
|
+
typeof services.InferenceServerService.listInferenceServer
|
|
21
|
+
>,
|
|
22
|
+
GetInferenceServer: services.InferenceServerService.getInferenceServer as ExtractUnaryRpc<
|
|
23
|
+
typeof services.InferenceServerService.getInferenceServer
|
|
24
|
+
>,
|
|
25
|
+
ListProject: services.ProjectService.listProject as ExtractUnaryRpc<
|
|
26
|
+
typeof services.ProjectService.listProject
|
|
27
|
+
>,
|
|
28
|
+
GetProject: services.ProjectService.getProject as ExtractUnaryRpc<
|
|
29
|
+
typeof services.ProjectService.getProject
|
|
30
|
+
>,
|
|
31
|
+
GetPipeline: services.PipelineService.getPipeline as ExtractUnaryRpc<
|
|
32
|
+
typeof services.PipelineService.getPipeline
|
|
33
|
+
>,
|
|
34
|
+
ListPipeline: services.PipelineService.listPipeline as ExtractUnaryRpc<
|
|
35
|
+
typeof services.PipelineService.listPipeline
|
|
36
|
+
>,
|
|
37
|
+
ListPipelineRun: services.PipelineRunService.listPipelineRun as ExtractUnaryRpc<
|
|
38
|
+
typeof services.PipelineRunService.listPipelineRun
|
|
39
|
+
>,
|
|
40
|
+
GetPipelineRun: services.PipelineRunService.getPipelineRun as ExtractUnaryRpc<
|
|
41
|
+
typeof services.PipelineRunService.getPipelineRun
|
|
42
|
+
>,
|
|
43
|
+
ListTriggerRun: services.TriggerRunService.listTriggerRun as ExtractUnaryRpc<
|
|
44
|
+
typeof services.TriggerRunService.listTriggerRun
|
|
45
|
+
>,
|
|
46
|
+
GetTriggerRun: services.TriggerRunService.getTriggerRun as ExtractUnaryRpc<
|
|
47
|
+
typeof services.TriggerRunService.getTriggerRun
|
|
48
|
+
>,
|
|
49
|
+
UpdateTriggerRun: (record: TriggerRun) =>
|
|
50
|
+
services.TriggerRunService.updateTriggerRun({ triggerRun: record }),
|
|
51
|
+
CreatePipelineRun: (record: PipelineRun) =>
|
|
52
|
+
services.PipelineRunService.createPipelineRun({ pipelineRun: record }),
|
|
53
|
+
UpdatePipelineRun: (record: PipelineRun) =>
|
|
54
|
+
services.PipelineRunService.updatePipelineRun({ pipelineRun: record }),
|
|
55
|
+
ListModel: services.ModelService.listModel as ExtractUnaryRpc<
|
|
56
|
+
typeof services.ModelService.listModel
|
|
57
|
+
>,
|
|
58
|
+
} as const;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Gets the RPC handlers, initializing them with runtime configuration on first call. */
|
|
62
|
+
export async function getRpcHandlers() {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
64
|
+
if (!handlersPromise) {
|
|
65
|
+
handlersPromise = createHandlers();
|
|
66
|
+
}
|
|
67
|
+
return handlersPromise;
|
|
68
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Export all files from gen/michelangelo/api
|
|
2
|
+
export * from './gen/michelangelo/api/conditions_pb';
|
|
3
|
+
export * from './gen/michelangelo/api/list_pb';
|
|
4
|
+
export * from './gen/michelangelo/api/options_pb';
|
|
5
|
+
export * from './gen/michelangelo/api/typed_struct_pb';
|
|
6
|
+
|
|
7
|
+
// Export all files from gen/michelangelo/api/v2
|
|
8
|
+
export * from './gen/michelangelo/api/v2/git_pb';
|
|
9
|
+
export * from './gen/michelangelo/api/v2/groupversion_info_pb';
|
|
10
|
+
export * from './gen/michelangelo/api/v2/job_pb';
|
|
11
|
+
export * from './gen/michelangelo/api/v2/model_pb';
|
|
12
|
+
export * from './gen/michelangelo/api/v2/model_svc_pb';
|
|
13
|
+
export * from './gen/michelangelo/api/v2/notification_pb';
|
|
14
|
+
export * from './gen/michelangelo/api/v2/parameter_pb';
|
|
15
|
+
export * from './gen/michelangelo/api/v2/pipeline_pb';
|
|
16
|
+
export * from './gen/michelangelo/api/v2/pipeline_run_pb';
|
|
17
|
+
export * from './gen/michelangelo/api/v2/pipeline_run_svc_pb';
|
|
18
|
+
export * from './gen/michelangelo/api/v2/pipeline_svc_pb';
|
|
19
|
+
export * from './gen/michelangelo/api/v2/pod_pb';
|
|
20
|
+
export * from './gen/michelangelo/api/v2/project_pb';
|
|
21
|
+
export * from './gen/michelangelo/api/v2/project_svc_pb';
|
|
22
|
+
export * from './gen/michelangelo/api/v2/ray_cluster_pb';
|
|
23
|
+
export * from './gen/michelangelo/api/v2/ray_cluster_svc_pb';
|
|
24
|
+
export * from './gen/michelangelo/api/v2/ray_job_pb';
|
|
25
|
+
export * from './gen/michelangelo/api/v2/ray_job_svc_pb';
|
|
26
|
+
export * from './gen/michelangelo/api/v2/schema_pb';
|
|
27
|
+
export * from './gen/michelangelo/api/v2/spark_job_pb';
|
|
28
|
+
export * from './gen/michelangelo/api/v2/spark_job_svc_pb';
|
|
29
|
+
export * from './gen/michelangelo/api/v2/trigger_run_pb';
|
|
30
|
+
export * from './gen/michelangelo/api/v2/trigger_run_svc_pb';
|
|
31
|
+
export * from './gen/michelangelo/api/v2/user_pb';
|
|
32
|
+
|
|
33
|
+
export { request } from './request';
|
|
34
|
+
export { normalizeConnectError } from './normalize-connect-error';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ConnectError } from '@connectrpc/connect';
|
|
2
|
+
import { ApplicationError, GrpcStatusCode } from '@michelangelo-ai/core';
|
|
3
|
+
|
|
4
|
+
import type { ErrorNormalizer } from '@michelangelo-ai/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes Connect RPC errors to ApplicationError format
|
|
8
|
+
*
|
|
9
|
+
* @param error - The error to normalize
|
|
10
|
+
* @returns ApplicationError if it's a Connect error, null otherwise
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Usage in error provider
|
|
15
|
+
* const errorProvider = (
|
|
16
|
+
* <ErrorProvider normalizeError={normalizeConnectError}>
|
|
17
|
+
* {children}
|
|
18
|
+
* </ErrorProvider>
|
|
19
|
+
* );
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const normalizeConnectError: ErrorNormalizer = (error: unknown): ApplicationError | null => {
|
|
23
|
+
if (!(error instanceof ConnectError)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new ApplicationError(error.message, mapConnectCodeToGrpc(error.code), {
|
|
28
|
+
source: 'connect-rpc',
|
|
29
|
+
meta: {
|
|
30
|
+
connectErrorName: error.name,
|
|
31
|
+
details: error.details,
|
|
32
|
+
metadata: error.metadata,
|
|
33
|
+
},
|
|
34
|
+
cause: error.cause ?? error,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Maps Connect RPC status codes to gRPC status codes
|
|
40
|
+
* Connect uses the same numeric codes as gRPC
|
|
41
|
+
*/
|
|
42
|
+
function mapConnectCodeToGrpc(code: number): GrpcStatusCode {
|
|
43
|
+
// Connect uses the same numeric codes as gRPC, so we can map directly
|
|
44
|
+
switch (code) {
|
|
45
|
+
case 0:
|
|
46
|
+
return GrpcStatusCode.OK;
|
|
47
|
+
case 1:
|
|
48
|
+
return GrpcStatusCode.CANCELLED;
|
|
49
|
+
case 2:
|
|
50
|
+
return GrpcStatusCode.UNKNOWN;
|
|
51
|
+
case 3:
|
|
52
|
+
return GrpcStatusCode.INVALID_ARGUMENT;
|
|
53
|
+
case 4:
|
|
54
|
+
return GrpcStatusCode.DEADLINE_EXCEEDED;
|
|
55
|
+
case 5:
|
|
56
|
+
return GrpcStatusCode.NOT_FOUND;
|
|
57
|
+
case 6:
|
|
58
|
+
return GrpcStatusCode.ALREADY_EXISTS;
|
|
59
|
+
case 7:
|
|
60
|
+
return GrpcStatusCode.PERMISSION_DENIED;
|
|
61
|
+
case 8:
|
|
62
|
+
return GrpcStatusCode.RESOURCE_EXHAUSTED;
|
|
63
|
+
case 9:
|
|
64
|
+
return GrpcStatusCode.FAILED_PRECONDITION;
|
|
65
|
+
case 10:
|
|
66
|
+
return GrpcStatusCode.ABORTED;
|
|
67
|
+
case 11:
|
|
68
|
+
return GrpcStatusCode.OUT_OF_RANGE;
|
|
69
|
+
case 12:
|
|
70
|
+
return GrpcStatusCode.UNIMPLEMENTED;
|
|
71
|
+
case 13:
|
|
72
|
+
return GrpcStatusCode.INTERNAL;
|
|
73
|
+
case 14:
|
|
74
|
+
return GrpcStatusCode.UNAVAILABLE;
|
|
75
|
+
case 15:
|
|
76
|
+
return GrpcStatusCode.DATA_LOSS;
|
|
77
|
+
case 16:
|
|
78
|
+
return GrpcStatusCode.UNAUTHENTICATED;
|
|
79
|
+
default:
|
|
80
|
+
return GrpcStatusCode.UNKNOWN;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@michelangelo-ai/rpc",
|
|
3
|
+
"license": "Apache-2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.3.0-nightly.20260701",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"test": "cd ../../ && yarn test:rpc"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
12
|
+
"vite": "6.2.0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@connectrpc/connect": "^2.0.2",
|
|
16
|
+
"@connectrpc/connect-web": "^2.0.2",
|
|
17
|
+
"@michelangelo-ai/core": "*"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"default": "./index.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"imports": {
|
|
25
|
+
"#rpc/*": "./"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/request.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getRpcHandlers } from './handlers';
|
|
2
|
+
|
|
3
|
+
import type { OmitTypeName, RpcHandlerType } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Makes a gRPC-web request to the Michelangelo API.
|
|
7
|
+
*
|
|
8
|
+
* Responses are decoded into plain objects — protobuf internals ($typeName, $unknown)
|
|
9
|
+
* are stripped recursively.
|
|
10
|
+
*
|
|
11
|
+
* @param rpcId - The ID of the RPC handler to call.
|
|
12
|
+
* @param args - The arguments to pass to the RPC handler.
|
|
13
|
+
* @returns A promise that resolves to the RPC response as a plain object.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const response = await request('ListProject', { /* project list args *\/ });
|
|
18
|
+
*
|
|
19
|
+
* // response is of type ListProjectResponse
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export async function request<RpcId extends keyof RpcHandlerType>(
|
|
23
|
+
rpcId: RpcId,
|
|
24
|
+
args: OmitTypeName<Parameters<RpcHandlerType[RpcId]>[0]>
|
|
25
|
+
): Promise<OmitTypeName<Awaited<ReturnType<RpcHandlerType[RpcId]>>>> {
|
|
26
|
+
const handlers = await getRpcHandlers();
|
|
27
|
+
const handler = handlers[rpcId] as (a: unknown) => Promise<unknown>;
|
|
28
|
+
const response = (await handler(args)) as Awaited<ReturnType<RpcHandlerType[RpcId]>>;
|
|
29
|
+
return toPlainObject(response) as OmitTypeName<Awaited<ReturnType<RpcHandlerType[RpcId]>>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Recursively strips protobuf internals ($typeName, $unknown) from a response,
|
|
33
|
+
// returning a plain object tree that is safe to spread and reconstruct into new requests.
|
|
34
|
+
function toPlainObject(value: unknown): unknown {
|
|
35
|
+
if (value === null || typeof value !== 'object') return value;
|
|
36
|
+
if (value instanceof Uint8Array) return value; // preserve bytes fields (e.g. google.protobuf.Any.value)
|
|
37
|
+
if (Array.isArray(value)) return value.map(toPlainObject);
|
|
38
|
+
|
|
39
|
+
const result: Record<string, unknown> = {};
|
|
40
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
41
|
+
if (key === '$typeName' || key === '$unknown') continue;
|
|
42
|
+
result[key] = toPlainObject(val);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RuntimeConfig } from './types';
|
|
2
|
+
|
|
3
|
+
// Fetches runtime configuration from /config.json.
|
|
4
|
+
export async function getRuntimeConfig(): Promise<RuntimeConfig> {
|
|
5
|
+
let response: Response;
|
|
6
|
+
try {
|
|
7
|
+
response = await fetch('/config.json');
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
10
|
+
console.error(`Config network error: ${error.message}`);
|
|
11
|
+
throw createConfigError('Check network connectivity.', { cause: error });
|
|
12
|
+
}
|
|
13
|
+
console.error(
|
|
14
|
+
`Config fetch error: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
15
|
+
);
|
|
16
|
+
throw createConfigError('Check that config.json is properly mounted.', { cause: error });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
console.error(`Config fetch failed: ${response.status} ${response.statusText}`);
|
|
21
|
+
throw createConfigError('Check that config.json is properly mounted.', { cause: response });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let config: RuntimeConfig;
|
|
25
|
+
try {
|
|
26
|
+
config = (await response.json()) as RuntimeConfig;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(
|
|
29
|
+
`Config JSON parsing failed: ${error instanceof Error ? error.message : 'Invalid JSON'}`
|
|
30
|
+
);
|
|
31
|
+
throw createConfigError('Check that config.json contains valid JSON.', { cause: error });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!config.apiBaseUrl) {
|
|
35
|
+
console.error('Config missing apiBaseUrl field', JSON.stringify(config));
|
|
36
|
+
throw createConfigError('Check that config.json contains apiBaseUrl field.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createConfigError(reason: string, options?: ErrorOptions): Error {
|
|
43
|
+
return new Error(`Failed to load runtime configuration. ${reason}`, options);
|
|
44
|
+
}
|
package/services.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRegistry } from '@bufbuild/protobuf';
|
|
2
|
+
import { createClient } from '@connectrpc/connect';
|
|
3
|
+
import { createConnectTransport } from '@connectrpc/connect-web';
|
|
4
|
+
|
|
5
|
+
import { TypedStructSchema } from './gen/michelangelo/api/typed_struct_pb';
|
|
6
|
+
import { DeploymentService } from './gen/michelangelo/api/v2/deployment_svc_pb';
|
|
7
|
+
import { InferenceServerService } from './gen/michelangelo/api/v2/inference_server_svc_pb';
|
|
8
|
+
import { ModelService } from './gen/michelangelo/api/v2/model_svc_pb';
|
|
9
|
+
import { PipelineRunService } from './gen/michelangelo/api/v2/pipeline_run_svc_pb';
|
|
10
|
+
import { PipelineService } from './gen/michelangelo/api/v2/pipeline_svc_pb';
|
|
11
|
+
import { ProjectService } from './gen/michelangelo/api/v2/project_svc_pb';
|
|
12
|
+
import { TriggerRunService } from './gen/michelangelo/api/v2/trigger_run_svc_pb';
|
|
13
|
+
import { getRuntimeConfig } from './runtime-config';
|
|
14
|
+
|
|
15
|
+
const typeRegistry = createRegistry(TypedStructSchema);
|
|
16
|
+
|
|
17
|
+
import type { Interceptor } from '@connectrpc/connect';
|
|
18
|
+
import type { Services } from './types';
|
|
19
|
+
|
|
20
|
+
// This interceptor is used to set the headers for the RPC request to
|
|
21
|
+
// be compatible with the Michelangelo API yarpc server.
|
|
22
|
+
const callerInterceptor: Interceptor = (next) => async (req) => {
|
|
23
|
+
req.header.set('context-Ttl-Ms', '10000');
|
|
24
|
+
req.header.set('grpc-timeout', '1000000m');
|
|
25
|
+
req.header.set('Rpc-Caller', 'ma-studio');
|
|
26
|
+
req.header.set('Rpc-Service', 'ma-apiserver');
|
|
27
|
+
|
|
28
|
+
return await next(req);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let servicesPromise: Promise<Services> | null = null;
|
|
32
|
+
|
|
33
|
+
async function createServices(): Promise<Services> {
|
|
34
|
+
const { apiBaseUrl } = await getRuntimeConfig();
|
|
35
|
+
|
|
36
|
+
const transport = createConnectTransport({
|
|
37
|
+
baseUrl: apiBaseUrl,
|
|
38
|
+
interceptors: [callerInterceptor],
|
|
39
|
+
jsonOptions: { registry: typeRegistry },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
DeploymentService: createClient(DeploymentService, transport),
|
|
44
|
+
InferenceServerService: createClient(InferenceServerService, transport),
|
|
45
|
+
ProjectService: createClient(ProjectService, transport),
|
|
46
|
+
PipelineService: createClient(PipelineService, transport),
|
|
47
|
+
PipelineRunService: createClient(PipelineRunService, transport),
|
|
48
|
+
TriggerRunService: createClient(TriggerRunService, transport),
|
|
49
|
+
ModelService: createClient(ModelService, transport),
|
|
50
|
+
} as const;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets the RPC services, initializing them with runtime configuration on first call.
|
|
55
|
+
*/
|
|
56
|
+
export async function getServices(): Promise<Services> {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
58
|
+
if (!servicesPromise) {
|
|
59
|
+
servicesPromise = createServices();
|
|
60
|
+
}
|
|
61
|
+
return servicesPromise;
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExtractEntityFromResponse, HasTypeName } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the main entity from a response
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* type MyResponse = {
|
|
9
|
+
* $typeName: 'michelangelo.api.v2.GetProjectResponse';
|
|
10
|
+
* project: Project;
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* expect(extractEntityFromResponse(myResponse)).toEqual(myResponse.project) ;
|
|
14
|
+
*
|
|
15
|
+
* // If the response is not a valid response, it will throw an error
|
|
16
|
+
* type MyResponse = {
|
|
17
|
+
* $typeName: 'some.other.api.v1.SomeOtherResponse';
|
|
18
|
+
* someOtherEntity: SomeOtherEntity;
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* expect(extractEntityFromResponse(myResponse)).toThrowError(
|
|
22
|
+
* 'Entity name someOtherEntity not found in response'
|
|
23
|
+
* );
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function extractEntityFromResponse<T extends HasTypeName>(
|
|
27
|
+
response: T
|
|
28
|
+
): ExtractEntityFromResponse<T> {
|
|
29
|
+
const typeName = response.$typeName;
|
|
30
|
+
const entityName = typeName
|
|
31
|
+
.replace(/^michelangelo\.api\.v2\.(Get|Create|Update)/, '')
|
|
32
|
+
.replace(/Response$/, '')
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (entityName in response) {
|
|
36
|
+
return response[entityName] as ExtractEntityFromResponse<T>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Entity name ${entityName} not found in response`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HasTypeName } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type predicate to check if an object has a $typeName property
|
|
5
|
+
*/
|
|
6
|
+
export function hasTypeName(value: unknown): value is HasTypeName {
|
|
7
|
+
return typeof value === 'object' && value !== null && '$typeName' in value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type guard to check if a response is an entity response (Get, Create, or Update)
|
|
12
|
+
*/
|
|
13
|
+
export function isSingularResponse<T>(value: T): value is T & HasTypeName {
|
|
14
|
+
return (
|
|
15
|
+
hasTypeName(value) &&
|
|
16
|
+
(value.$typeName.startsWith('michelangelo.api.v2.Get') ||
|
|
17
|
+
value.$typeName.startsWith('michelangelo.api.v2.Create') ||
|
|
18
|
+
value.$typeName.startsWith('michelangelo.api.v2.Update'))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description
|
|
3
|
+
* Type for objects that have a $typeName property. $typeName is added by the `@bufbuild/protobuf` library.
|
|
4
|
+
* This type is used to identify the associated protobuf message type.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* type GetProjectResponse = {
|
|
9
|
+
* $typeName: 'michelangelo.api.v2.GetProjectResponse';
|
|
10
|
+
* project: Project;
|
|
11
|
+
* };
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export type HasTypeName = {
|
|
15
|
+
$typeName: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @description
|
|
20
|
+
* Singular endpoints are endpoints that return a single entity.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* type GetProjectResponse = {
|
|
25
|
+
* $typeName: 'michelangelo.api.v2.GetProjectResponse';
|
|
26
|
+
* project: Project;
|
|
27
|
+
* };
|
|
28
|
+
*/
|
|
29
|
+
type SingularEndpoint = 'Get' | 'Create' | 'Update';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @description
|
|
33
|
+
* Delete endpoint is the endpoint that deletes an entity.
|
|
34
|
+
*/
|
|
35
|
+
type DeleteEndpoint = 'Delete';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @description
|
|
39
|
+
* List endpoint is the endpoint that returns a list of entities.
|
|
40
|
+
*/
|
|
41
|
+
type ListEndpoint = 'List';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @description
|
|
45
|
+
* Type for the endpoint part of the $typeName property.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* // Endpoint can be used in a type guard to check if a response typeName
|
|
50
|
+
* // belongs to a known endpoint
|
|
51
|
+
* type IsProjectResponse<T extends string> =
|
|
52
|
+
* T extends `michelangelo.api.v2.${Endpoint}ProjectResponse` ? true : false;
|
|
53
|
+
*
|
|
54
|
+
* type IsProjectResponseResult = IsProjectResponse<'michelangelo.api.v2.GetProjectResponse'>;
|
|
55
|
+
* // => true -- Get is not a known endpoint
|
|
56
|
+
*
|
|
57
|
+
* type IsProjectResponseResult = IsProjectResponse<'michelangelo.api.v2.RetryProjectResponse'>;
|
|
58
|
+
* // => false -- Retry is not a known endpoint
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
type Endpoint = SingularEndpoint | ListEndpoint | DeleteEndpoint;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @description
|
|
65
|
+
* Extracts the lowercased entity name from an absolute path to the response protobuf message.
|
|
66
|
+
* The resulting entity name should be used as the key to access the entity from the response object.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* This assumes that the response protobuf message is in the `michelangelo.api.v2` namespace.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* type GetProjectResponse = {
|
|
74
|
+
* $typeName: 'michelangelo.api.v2.GetProjectResponse';
|
|
75
|
+
* project: Project;
|
|
76
|
+
* };
|
|
77
|
+
*
|
|
78
|
+
* ExtractEntityName<'michelangelo.api.v2.GetProjectResponse'>;
|
|
79
|
+
* // => 'project'
|
|
80
|
+
*
|
|
81
|
+
* ExtractEntityName<'some.other.protobuf.GetProjectResponse'>;
|
|
82
|
+
* // => never
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
type ExtractEntityName<T extends string> =
|
|
86
|
+
T extends `michelangelo.api.v2.${Endpoint}${infer Entity}Response` ? Lowercase<Entity> : never;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @description
|
|
90
|
+
* Extracts the inner entity from a protobuf response message.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* type GetProjectResponse = {
|
|
95
|
+
* $typeName: 'michelangelo.api.v2.GetProjectResponse';
|
|
96
|
+
* project: Project;
|
|
97
|
+
* };
|
|
98
|
+
*
|
|
99
|
+
* ExtractEntityFromResponse<GetProjectResponse>;
|
|
100
|
+
* // => Project
|
|
101
|
+
*
|
|
102
|
+
* type ListProjectsResponse = {
|
|
103
|
+
* $typeName: 'michelangelo.api.v2.ListProjectsResponse';
|
|
104
|
+
* projects: Project[];
|
|
105
|
+
* };
|
|
106
|
+
*
|
|
107
|
+
* ExtractEntityFromResponse<ListProjectsResponse>;
|
|
108
|
+
* // => Project[]
|
|
109
|
+
*
|
|
110
|
+
* type SomeOtherResponse = {
|
|
111
|
+
* $typeName: 'some.other.protobuf.SomeOtherResponse';
|
|
112
|
+
* someField: string;
|
|
113
|
+
* };
|
|
114
|
+
*
|
|
115
|
+
* ExtractEntityFromResponse<SomeOtherResponse>;
|
|
116
|
+
* // => SomeOtherResponse
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export type ExtractEntityFromResponse<T> = T extends HasTypeName
|
|
120
|
+
? T extends Partial<Record<ExtractEntityName<T['$typeName']>, infer E>>
|
|
121
|
+
? E
|
|
122
|
+
: T
|
|
123
|
+
: T;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"include": ["."],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"composite": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"emitDeclarationOnly": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"noEmit": false,
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"rootDir": "."
|
|
13
|
+
}
|
|
14
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Message } from '@bufbuild/protobuf';
|
|
2
|
+
import type { createClient } from '@connectrpc/connect';
|
|
3
|
+
import type { DeploymentService } from './gen/michelangelo/api/v2/deployment_svc_pb';
|
|
4
|
+
import type { InferenceServerService } from './gen/michelangelo/api/v2/inference_server_svc_pb';
|
|
5
|
+
import type { ModelService } from './gen/michelangelo/api/v2/model_svc_pb';
|
|
6
|
+
import type { PipelineRunService } from './gen/michelangelo/api/v2/pipeline_run_svc_pb';
|
|
7
|
+
import type { PipelineService } from './gen/michelangelo/api/v2/pipeline_svc_pb';
|
|
8
|
+
import type { ProjectService } from './gen/michelangelo/api/v2/project_svc_pb';
|
|
9
|
+
import type { TriggerRunService } from './gen/michelangelo/api/v2/trigger_run_svc_pb';
|
|
10
|
+
import type { getRpcHandlers } from './handlers';
|
|
11
|
+
|
|
12
|
+
export interface RuntimeConfig {
|
|
13
|
+
apiBaseUrl: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Services = {
|
|
17
|
+
DeploymentService: ReturnType<typeof createClient<typeof DeploymentService>>;
|
|
18
|
+
InferenceServerService: ReturnType<typeof createClient<typeof InferenceServerService>>;
|
|
19
|
+
ProjectService: ReturnType<typeof createClient<typeof ProjectService>>;
|
|
20
|
+
PipelineService: ReturnType<typeof createClient<typeof PipelineService>>;
|
|
21
|
+
PipelineRunService: ReturnType<typeof createClient<typeof PipelineRunService>>;
|
|
22
|
+
TriggerRunService: ReturnType<typeof createClient<typeof TriggerRunService>>;
|
|
23
|
+
ModelService: ReturnType<typeof createClient<typeof ModelService>>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @see {@link getRpcHandlers}
|
|
28
|
+
*/
|
|
29
|
+
export type RpcHandlerType = Awaited<ReturnType<typeof getRpcHandlers>>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @description
|
|
33
|
+
* Extracts the unary-unary function type from the RPC handler type.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* The Connect Client type generates a type that includes unary-unary, unary-server-streaming,
|
|
37
|
+
* unary-client-streaming, and unary-bidi-streaming functions. We want to extract the
|
|
38
|
+
* unary-unary function type from the RPC handler type.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* getProject: (args: { projectId: string }) => Promise<Project> | AsyncIterable<Project>;
|
|
43
|
+
* ExtractUnaryRpc<getProject>
|
|
44
|
+
* // => (args: { projectId: string }) => Promise<Project>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export type ExtractUnaryRpc<T> = T extends (args: Record<string, unknown>) => Promise<infer R>
|
|
48
|
+
? (args: Record<string, unknown>) => Promise<R>
|
|
49
|
+
: never;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @description
|
|
53
|
+
* Removes the `$typeName` and `$unknown` properties from a message. These are properties
|
|
54
|
+
* that are added by the protobuf-es library. We don't need them for our RPC calls.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* type MyMessage = {
|
|
59
|
+
* $typeName: string;
|
|
60
|
+
* $unknown: unknown;
|
|
61
|
+
* myField: string;
|
|
62
|
+
* };
|
|
63
|
+
*
|
|
64
|
+
* type MyMessageWithoutTypeName = OmitTypeName<MyMessage>;
|
|
65
|
+
* const message: MyMessageWithoutTypeName = { myField: 'hello' };
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @see https://github.com/bufbuild/protobuf-es/issues/1016
|
|
69
|
+
*/
|
|
70
|
+
export type OmitTypeName<T> = {
|
|
71
|
+
[P in keyof T as P extends '$typeName' | '$unknown' ? never : P]: Recurse<T[P]>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type Recurse<F> = F extends (infer U)[]
|
|
75
|
+
? Recurse<U>[]
|
|
76
|
+
: F extends Message
|
|
77
|
+
? OmitTypeName<F>
|
|
78
|
+
: F extends { case: infer C extends string; value: infer V extends Message }
|
|
79
|
+
? { case: C; value: OmitTypeName<V> }
|
|
80
|
+
: F extends Record<string, infer V extends Message>
|
|
81
|
+
? Record<string, OmitTypeName<V>>
|
|
82
|
+
: F extends Record<number, infer V extends Message>
|
|
83
|
+
? Record<number, OmitTypeName<V>>
|
|
84
|
+
: F;
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
build: {
|
|
7
|
+
lib: {
|
|
8
|
+
entry: path.resolve(__dirname, 'index.ts'),
|
|
9
|
+
name: 'MichelangeloRpc',
|
|
10
|
+
formats: ['es'],
|
|
11
|
+
},
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
external: [
|
|
14
|
+
'react',
|
|
15
|
+
'@bufbuild/protobuf',
|
|
16
|
+
'@connectrpc/connect',
|
|
17
|
+
'@connectrpc/connect-web',
|
|
18
|
+
'@tanstack/react-query',
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
outDir: 'dist',
|
|
22
|
+
emptyOutDir: true,
|
|
23
|
+
},
|
|
24
|
+
plugins: [react()],
|
|
25
|
+
});
|