History

Use tagged errors for local runtime failures

Rhys Sullivan committed 8 months ago

Commit f858ab81 parent df6aa378 files · +235 −48Browse files

Unified · Split

8 files
  1. packages/control-plane/src/api/local-context.ts +7 −1
  2. packages/control-plane/src/runtime/local-config.ts +40 −13
  3. packages/control-plane/src/runtime/local-errors.ts +54 −0
  4. packages/control-plane/src/runtime/local-runtime-context.ts +25 −8
  5. packages/control-plane/src/runtime/local-source-artifacts.ts +23 −9
  6. packages/control-plane/src/runtime/local-workspace-state.ts +19 −8
  7. packages/control-plane/src/runtime/source-recipes-runtime.ts +6 −2
  8. packages/control-plane/src/runtime/source-store.ts +61 −7
packages/control-plane/src/api/local-context.ts+7−1
@@ -1,6 +1,9 @@
1import type { WorkspaceId } from "#schema";1import type { WorkspaceId } from "#schema";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import {
5 RuntimeLocalWorkspaceMismatchError,
6} from "../runtime/local-errors";
4import { requireRuntimeLocalWorkspace } from "../runtime/local-runtime-context";7import { requireRuntimeLocalWorkspace } from "../runtime/local-runtime-context";
5import { ControlPlaneForbiddenError } from "./errors";8import { ControlPlaneForbiddenError } from "./errors";
6 9
@@ -13,7 +16,10 @@ export const resolveRequestedLocalWorkspace = ( · 6 unmodified lines
13 new ControlPlaneForbiddenError({16 new ControlPlaneForbiddenError({
14 operation,17 operation,
15 message: "Requested workspace is not the active local workspace",18 message: "Requested workspace is not the active local workspace",
16 details: cause instanceof Error ? cause.message : String(cause),19 details:
20 cause instanceof RuntimeLocalWorkspaceMismatchError
21 ? `requestedWorkspaceId=${cause.requestedWorkspaceId} activeWorkspaceId=${cause.activeWorkspaceId}`
22 : "Runtime local workspace is unavailable",
17 })23 })
18 ),24 ),
19 );25 );
packages/control-plane/src/runtime/local-config.ts+40−13
@@ -17,6 +17,11 @@ import { · 16 unmodified lines
17} from "#schema";17} from "#schema";
18import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
19import * as Schema from "effect/Schema";19import * as Schema from "effect/Schema";
20import {
21 LocalExecutorConfigDecodeError,
22 LocalFileSystemError,
23 unknownLocalErrorDetails,
24} from "./local-errors";
20 25
21const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema);26const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema);
22 27
@@ -34,10 +39,13 @@ const provideNodeFileSystem = <A, E, R>( · 11 unmodified lines
34 Exclude<R, FileSystem.FileSystem>39 Exclude<R, FileSystem.FileSystem>
35 >;40 >;
36 41
37const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => {42const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
38 const message = cause instanceof Error ? cause.message : String(cause);43 new LocalFileSystemError({
39 return new Error(`Failed to ${action} ${path}: ${message}`);44 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
40};45 action,
46 path,
47 details: unknownLocalErrorDetails(cause),
48 });
41 49
42const trimOrUndefined = (value: string | undefined | null): string | undefined => {50const trimOrUndefined = (value: string | undefined | null): string | undefined => {
43 const trimmed = value?.trim();51 const trimmed = value?.trim();
@@ -106,7 +114,7 @@ export const resolveHomeConfigPath = (input: { · 62 unmodified lines
106 env?: NodeJS.ProcessEnv;114 env?: NodeJS.ProcessEnv;
107 platform?: NodeJS.Platform;115 platform?: NodeJS.Platform;
108 homeDirectory?: string;116 homeDirectory?: string;
109} = {}): Effect.Effect<string, Error> =>117} = {}): Effect.Effect<string, LocalFileSystemError> =>
110 provideNodeFileSystem(Effect.gen(function* () {118 provideNodeFileSystem(Effect.gen(function* () {
111 const fs = yield* FileSystem.FileSystem;119 const fs = yield* FileSystem.FileSystem;
112 const candidates = resolveDefaultHomeConfigCandidates(input);120 const candidates = resolveDefaultHomeConfigCandidates(input);
@@ -152,13 +160,23 @@ const parseJsonc = (input: { path: string; content: string }): LocalExecutorConf · 39 unmodified lines
152 allowTrailingComma: true,160 allowTrailingComma: true,
153 });161 });
154 if (errors.length > 0) {162 if (errors.length > 0) {
155 throw new Error(formatJsoncParseErrors(input.content, errors));163 throw new LocalExecutorConfigDecodeError({
164 message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`,
165 path: input.path,
166 details: formatJsoncParseErrors(input.content, errors),
167 });
156 }168 }
157 169
158 return decodeLocalExecutorConfig(parsed);170 return decodeLocalExecutorConfig(parsed);
159 } catch (cause) {171 } catch (cause) {
160 const message = cause instanceof Error ? cause.message : String(cause);172 if (cause instanceof LocalExecutorConfigDecodeError) {
161 throw new Error(`Invalid executor config at ${input.path}: ${message}`);173 throw cause;
174 }
175 throw new LocalExecutorConfigDecodeError({
176 message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`,
177 path: input.path,
178 details: unknownLocalErrorDetails(cause),
179 });
162 }180 }
163};181};
164 182
@@ -320,7 +338,7 @@ export type LoadedLocalExecutorConfig = { · 155 unmodified lines
320export const resolveLocalWorkspaceContext = (input: {338export const resolveLocalWorkspaceContext = (input: {
321 cwd?: string;339 cwd?: string;
322 workspaceRoot?: string;340 workspaceRoot?: string;
323} = {}): Effect.Effect<ResolvedLocalWorkspaceContext, Error> =>341} = {}): Effect.Effect<ResolvedLocalWorkspaceContext, LocalFileSystemError> =>
324 provideNodeFileSystem(Effect.gen(function* () {342 provideNodeFileSystem(Effect.gen(function* () {
325 const cwd = resolve(input.cwd ?? process.cwd());343 const cwd = resolve(input.cwd ?? process.cwd());
326 const workspaceRoot = resolve(344 const workspaceRoot = resolve(
@@ -344,7 +362,7 @@ export const resolveLocalWorkspaceContext = (input: { · 17 unmodified lines
344 362
345export const readOptionalLocalExecutorConfig = (363export const readOptionalLocalExecutorConfig = (
346 path: string,364 path: string,
347): Effect.Effect<LocalExecutorConfig | null, Error> =>365): Effect.Effect<LocalExecutorConfig | null, LocalFileSystemError | LocalExecutorConfigDecodeError> =>
348 provideNodeFileSystem(Effect.gen(function* () {366 provideNodeFileSystem(Effect.gen(function* () {
349 const fs = yield* FileSystem.FileSystem;367 const fs = yield* FileSystem.FileSystem;
350 const exists = yield* fs.exists(path).pipe(368 const exists = yield* fs.exists(path).pipe(
@@ -360,13 +378,22 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines
360 return yield* Effect.try({378 return yield* Effect.try({
361 try: () => parseJsonc({ path, content }),379 try: () => parseJsonc({ path, content }),
362 catch: (cause) =>380 catch: (cause) =>
363 cause instanceof Error ? cause : new Error(String(cause)),381 cause instanceof LocalExecutorConfigDecodeError
382 ? cause
383 : new LocalExecutorConfigDecodeError({
384 message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,
385 path,
386 details: unknownLocalErrorDetails(cause),
387 }),
364 });388 });
365 }));389 }));
366 390
367export const loadLocalExecutorConfig = (391export const loadLocalExecutorConfig = (
368 context: ResolvedLocalWorkspaceContext,392 context: ResolvedLocalWorkspaceContext,
369): Effect.Effect<LoadedLocalExecutorConfig, Error> =>393): Effect.Effect<
394 LoadedLocalExecutorConfig,
395 LocalFileSystemError | LocalExecutorConfigDecodeError
396> =>
370 Effect.gen(function* () {397 Effect.gen(function* () {
371 const [homeConfig, projectConfig] = yield* Effect.all([398 const [homeConfig, projectConfig] = yield* Effect.all([
372 readOptionalLocalExecutorConfig(context.homeConfigPath),399 readOptionalLocalExecutorConfig(context.homeConfigPath),
@@ -388,7 +415,7 @@ export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string = · 15 unmodified lines
388export const writeProjectLocalExecutorConfig = (input: {415export const writeProjectLocalExecutorConfig = (input: {
389 context: ResolvedLocalWorkspaceContext;416 context: ResolvedLocalWorkspaceContext;
390 config: LocalExecutorConfig;417 config: LocalExecutorConfig;
391}): Effect.Effect<void, Error> =>418}): Effect.Effect<void, LocalFileSystemError> =>
392 provideNodeFileSystem(Effect.gen(function* () {419 provideNodeFileSystem(Effect.gen(function* () {
393 const fs = yield* FileSystem.FileSystem;420 const fs = yield* FileSystem.FileSystem;
394 yield* fs.makeDirectory(input.context.configDirectory, { recursive: true }).pipe(421 yield* fs.makeDirectory(input.context.configDirectory, { recursive: true }).pipe(
packages/control-plane/src/runtime/local-errors.ts+54−0
@@ -0,0 +1,54 @@
1import * as Data from "effect/Data";
2
3export const unknownLocalErrorDetails = (cause: unknown): string =>
4 cause instanceof Error ? cause.message : String(cause);
5
6export class LocalFileSystemError extends Data.TaggedError("LocalFileSystemError")<{
7 readonly message: string;
8 readonly action: string;
9 readonly path: string;
10 readonly details: string;
11}> {}
12
13export class LocalExecutorConfigDecodeError extends Data.TaggedError("LocalExecutorConfigDecodeError")<{
14 readonly message: string;
15 readonly path: string;
16 readonly details: string;
17}> {}
18
19export class LocalWorkspaceStateDecodeError extends Data.TaggedError("LocalWorkspaceStateDecodeError")<{
20 readonly message: string;
21 readonly path: string;
22 readonly details: string;
23}> {}
24
25export class LocalSourceArtifactDecodeError extends Data.TaggedError("LocalSourceArtifactDecodeError")<{
26 readonly message: string;
27 readonly path: string;
28 readonly details: string;
29}> {}
30
31export class RuntimeLocalWorkspaceUnavailableError extends Data.TaggedError("RuntimeLocalWorkspaceUnavailableError")<{
32 readonly message: string;
33}> {}
34
35export class RuntimeLocalWorkspaceMismatchError extends Data.TaggedError("RuntimeLocalWorkspaceMismatchError")<{
36 readonly message: string;
37 readonly requestedWorkspaceId: string;
38 readonly activeWorkspaceId: string;
39}> {}
40
41export class LocalConfiguredSourceNotFoundError extends Data.TaggedError("LocalConfiguredSourceNotFoundError")<{
42 readonly message: string;
43 readonly sourceId: string;
44}> {}
45
46export class LocalSourceArtifactMissingError extends Data.TaggedError("LocalSourceArtifactMissingError")<{
47 readonly message: string;
48 readonly sourceId: string;
49}> {}
50
51export class LocalUnsupportedSourceKindError extends Data.TaggedError("LocalUnsupportedSourceKindError")<{
52 readonly message: string;
53 readonly kind: string;
54}> {}
packages/control-plane/src/runtime/local-runtime-context.ts+25−8
@@ -7,6 +7,10 @@ import type { · 6 unmodified lines
7 LoadedLocalExecutorConfig,7 LoadedLocalExecutorConfig,
8 ResolvedLocalWorkspaceContext,8 ResolvedLocalWorkspaceContext,
9} from "./local-config";9} from "./local-config";
10import {
11 RuntimeLocalWorkspaceMismatchError,
12 RuntimeLocalWorkspaceUnavailableError,
13} from "./local-errors";
10 14
11export type RuntimeLocalWorkspaceState = {15export type RuntimeLocalWorkspaceState = {
12 context: ResolvedLocalWorkspaceContext;16 context: ResolvedLocalWorkspaceContext;
@@ -28,24 +32,37 @@ export const getRuntimeLocalWorkspaceOption = () => · 15 unmodified lines
28 Effect.map((option) => (Option.isSome(option) ? option.value : null)),32 Effect.map((option) => (Option.isSome(option) ? option.value : null)),
29 ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>;33 ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>;
30 34
31export const requireRuntimeLocalWorkspace = (workspaceId?: WorkspaceId) =>35export const requireRuntimeLocalWorkspace = (
32 Effect.flatMap(getRuntimeLocalWorkspaceOption(), (runtimeLocalWorkspace) => {36 workspaceId?: WorkspaceId,
37): Effect.Effect<
38 RuntimeLocalWorkspaceState,
39 RuntimeLocalWorkspaceUnavailableError | RuntimeLocalWorkspaceMismatchError,
40 never
41> =>
42 Effect.gen(function* () {
43 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
33 if (runtimeLocalWorkspace === null) {44 if (runtimeLocalWorkspace === null) {
34 return Effect.fail(new Error("Runtime local workspace is unavailable"));45 return yield* Effect.fail(
46 new RuntimeLocalWorkspaceUnavailableError({
47 message: "Runtime local workspace is unavailable",
48 }),
49 );
35 }50 }
36 51
37 if (52 if (
38 workspaceId !== undefined53 workspaceId !== undefined
39 && runtimeLocalWorkspace.installation.workspaceId !== workspaceId54 && runtimeLocalWorkspace.installation.workspaceId !== workspaceId
40 ) {55 ) {
41 return Effect.fail(56 return yield* Effect.fail(
42 new Error(57 new RuntimeLocalWorkspaceMismatchError({
43 `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`,58 message: `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`,
44 ),59 requestedWorkspaceId: workspaceId,
60 activeWorkspaceId: runtimeLocalWorkspace.installation.workspaceId,
61 }),
45 );62 );
46 }63 }
47 64
48 return Effect.succeed(runtimeLocalWorkspace);65 return runtimeLocalWorkspace;
49 });66 });
50 67
51export const requireRuntimeLocalAccountId = (workspaceId?: WorkspaceId) =>68export const requireRuntimeLocalAccountId = (workspaceId?: WorkspaceId) =>
packages/control-plane/src/runtime/local-source-artifacts.ts+23−9
@@ -17,6 +17,11 @@ import * as Effect from "effect/Effect"; · 16 unmodified lines
17import * as Schema from "effect/Schema";17import * as Schema from "effect/Schema";
18 18
19import type { ResolvedLocalWorkspaceContext } from "./local-config";19import type { ResolvedLocalWorkspaceContext } from "./local-config";
20import {
21 LocalFileSystemError,
22 LocalSourceArtifactDecodeError,
23 unknownLocalErrorDetails,
24} from "./local-errors";
20import {25import {
21 createSourceRecipeRevisionRecord,26 createSourceRecipeRevisionRecord,
22 stableSourceRecipeId,27 stableSourceRecipeId,
@@ -52,10 +57,13 @@ const provideNodeFileSystem = <A, E, R>( · 29 unmodified lines
52 Exclude<R, FileSystem.FileSystem>57 Exclude<R, FileSystem.FileSystem>
53 >;58 >;
54 59
55const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => {60const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
56 const message = cause instanceof Error ? cause.message : String(cause);61 new LocalFileSystemError({
57 return new Error(`Failed to ${action} ${path}: ${message}`);62 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
58};63 action,
64 path,
65 details: unknownLocalErrorDetails(cause),
66 });
59 67
60const localSourceArtifactPath = (input: {68const localSourceArtifactPath = (input: {
61 context: ResolvedLocalWorkspaceContext;69 context: ResolvedLocalWorkspaceContext;
@@ -157,7 +165,10 @@ export const buildLocalSourceArtifact = (input: { · 95 unmodified lines
157export const readLocalSourceArtifact = (input: {165export const readLocalSourceArtifact = (input: {
158 context: ResolvedLocalWorkspaceContext;166 context: ResolvedLocalWorkspaceContext;
159 sourceId: string;167 sourceId: string;
160}): Effect.Effect<LocalSourceArtifact | null, Error> =>168}): Effect.Effect<
169 LocalSourceArtifact | null,
170 LocalFileSystemError | LocalSourceArtifactDecodeError
171> =>
161 provideNodeFileSystem(Effect.gen(function* () {172 provideNodeFileSystem(Effect.gen(function* () {
162 const fs = yield* FileSystem.FileSystem;173 const fs = yield* FileSystem.FileSystem;
163 const path = localSourceArtifactPath(input);174 const path = localSourceArtifactPath(input);
@@ -174,8 +185,11 @@ export const readLocalSourceArtifact = (input: { · 10 unmodified lines
174 return yield* Effect.try({185 return yield* Effect.try({
175 try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown),186 try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown),
176 catch: (cause) => {187 catch: (cause) => {
177 const message = cause instanceof Error ? cause.message : String(cause);188 return new LocalSourceArtifactDecodeError({
178 return new Error(`Invalid local source artifact at ${path}: ${message}`);189 message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`,
190 path,
191 details: unknownLocalErrorDetails(cause),
192 });
179 },193 },
180 });194 });
181 }));195 }));
@@ -184,7 +198,7 @@ export const writeLocalSourceArtifact = (input: { · 2 unmodified lines
184 context: ResolvedLocalWorkspaceContext;198 context: ResolvedLocalWorkspaceContext;
185 sourceId: string;199 sourceId: string;
186 artifact: LocalSourceArtifact;200 artifact: LocalSourceArtifact;
187}): Effect.Effect<void, Error> =>201}): Effect.Effect<void, LocalFileSystemError> =>
188 provideNodeFileSystem(Effect.gen(function* () {202 provideNodeFileSystem(Effect.gen(function* () {
189 const fs = yield* FileSystem.FileSystem;203 const fs = yield* FileSystem.FileSystem;
190 const directory = join(input.context.artifactsDirectory, "sources");204 const directory = join(input.context.artifactsDirectory, "sources");
@@ -200,7 +214,7 @@ export const writeLocalSourceArtifact = (input: { · 9 unmodified lines
200export const removeLocalSourceArtifact = (input: {214export const removeLocalSourceArtifact = (input: {
201 context: ResolvedLocalWorkspaceContext;215 context: ResolvedLocalWorkspaceContext;
202 sourceId: string;216 sourceId: string;
203}): Effect.Effect<void, Error> =>217}): Effect.Effect<void, LocalFileSystemError> =>
204 provideNodeFileSystem(Effect.gen(function* () {218 provideNodeFileSystem(Effect.gen(function* () {
205 const fs = yield* FileSystem.FileSystem;219 const fs = yield* FileSystem.FileSystem;
206 const path = localSourceArtifactPath(input);220 const path = localSourceArtifactPath(input);
packages/control-plane/src/runtime/local-workspace-state.ts+19−8
@@ -11,6 +11,11 @@ import * as Effect from "effect/Effect"; · 10 unmodified lines
11import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
12 12
13import type { ResolvedLocalWorkspaceContext } from "./local-config";13import type { ResolvedLocalWorkspaceContext } from "./local-config";
14import {
15 LocalFileSystemError,
16 LocalWorkspaceStateDecodeError,
17 unknownLocalErrorDetails,
18} from "./local-errors";
14 19
15const WORKSPACE_STATE_BASENAME = "workspace-state.json";20const WORKSPACE_STATE_BASENAME = "workspace-state.json";
16 21
@@ -55,10 +60,13 @@ const provideNodeFileSystem = <A, E, R>( · 38 unmodified lines
55 Exclude<R, FileSystem.FileSystem>60 Exclude<R, FileSystem.FileSystem>
56 >;61 >;
57 62
58const mapFileSystemError = (path: string, action: string) => (cause: unknown): Error => {63const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
59 const message = cause instanceof Error ? cause.message : String(cause);64 new LocalFileSystemError({
60 return new Error(`Failed to ${action} ${path}: ${message}`);65 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
61};66 action,
67 path,
68 details: unknownLocalErrorDetails(cause),
69 });
62 70
63const defaultLocalWorkspaceState = (): LocalWorkspaceState => ({71const defaultLocalWorkspaceState = (): LocalWorkspaceState => ({
64 version: 1,72 version: 1,
@@ -72,7 +80,7 @@ export const localWorkspaceStatePath = ( · 7 unmodified lines
72 80
73export const loadLocalWorkspaceState = (81export const loadLocalWorkspaceState = (
74 context: ResolvedLocalWorkspaceContext,82 context: ResolvedLocalWorkspaceContext,
75): Effect.Effect<LocalWorkspaceState, Error> =>83): Effect.Effect<LocalWorkspaceState, LocalFileSystemError | LocalWorkspaceStateDecodeError> =>
76 provideNodeFileSystem(Effect.gen(function* () {84 provideNodeFileSystem(Effect.gen(function* () {
77 const fs = yield* FileSystem.FileSystem;85 const fs = yield* FileSystem.FileSystem;
78 const path = localWorkspaceStatePath(context);86 const path = localWorkspaceStatePath(context);
@@ -89,8 +97,11 @@ export const loadLocalWorkspaceState = ( · 10 unmodified lines
89 return yield* Effect.try({97 return yield* Effect.try({
90 try: () => decodeLocalWorkspaceState(JSON.parse(content) as unknown),98 try: () => decodeLocalWorkspaceState(JSON.parse(content) as unknown),
91 catch: (cause) => {99 catch: (cause) => {
92 const message = cause instanceof Error ? cause.message : String(cause);100 return new LocalWorkspaceStateDecodeError({
93 return new Error(`Invalid local workspace state at ${path}: ${message}`);101 message: `Invalid local workspace state at ${path}: ${unknownLocalErrorDetails(cause)}`,
102 path,
103 details: unknownLocalErrorDetails(cause),
104 });
94 },105 },
95 });106 });
96 }));107 }));
@@ -98,7 +109,7 @@ export const loadLocalWorkspaceState = ( · 1 unmodified line
98export const writeLocalWorkspaceState = (input: {109export const writeLocalWorkspaceState = (input: {
99 context: ResolvedLocalWorkspaceContext;110 context: ResolvedLocalWorkspaceContext;
100 state: LocalWorkspaceState;111 state: LocalWorkspaceState;
101}): Effect.Effect<void, Error> =>112}): Effect.Effect<void, LocalFileSystemError> =>
102 provideNodeFileSystem(Effect.gen(function* () {113 provideNodeFileSystem(Effect.gen(function* () {
103 const fs = yield* FileSystem.FileSystem;114 const fs = yield* FileSystem.FileSystem;
104 yield* fs.makeDirectory(input.context.stateDirectory, { recursive: true }).pipe(115 yield* fs.makeDirectory(input.context.stateDirectory, { recursive: true }).pipe(
packages/control-plane/src/runtime/source-recipes-runtime.ts+6−2
@@ -17,6 +17,7 @@ import * as Effect from "effect/Effect"; · 16 unmodified lines
17import {17import {
18 readLocalSourceArtifact,18 readLocalSourceArtifact,
19} from "./local-source-artifacts";19} from "./local-source-artifacts";
20import { LocalSourceArtifactMissingError } from "./local-errors";
20import {21import {
21 requireRuntimeLocalWorkspace,22 requireRuntimeLocalWorkspace,
22} from "./local-runtime-context";23} from "./local-runtime-context";
@@ -218,7 +219,7 @@ export const loadSourceWithRecipe = (input: { · 195 unmodified lines
218 workspaceId: WorkspaceId;219 workspaceId: WorkspaceId;
219 sourceId: Source["id"];220 sourceId: Source["id"];
220 actorAccountId?: AccountId | null;221 actorAccountId?: AccountId | null;
221}): Effect.Effect<LoadedSourceRecipe, Error, never> =>222}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> =>
222 Effect.gen(function* () {223 Effect.gen(function* () {
223 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);224 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId);
224 const source = yield* loadSourceById(input.rows, {225 const source = yield* loadSourceById(input.rows, {
@@ -232,7 +233,10 @@ export const loadSourceWithRecipe = (input: { · 7 unmodified lines
232 });233 });
233 if (artifact === null) {234 if (artifact === null) {
234 return yield* Effect.fail(235 return yield* Effect.fail(
235 new Error(`Recipe artifact missing for source ${input.sourceId}`),236 new LocalSourceArtifactMissingError({
237 message: `Recipe artifact missing for source ${input.sourceId}`,
238 sourceId: input.sourceId,
239 }),
236 );240 );
237 }241 }
238 242
packages/control-plane/src/runtime/source-store.ts+61−7
@@ -30,6 +30,15 @@ import { · 29 unmodified lines
30 readLocalSourceArtifact,30 readLocalSourceArtifact,
31 removeLocalSourceArtifact,31 removeLocalSourceArtifact,
32} from "./local-source-artifacts";32} from "./local-source-artifacts";
33import {
34 LocalConfiguredSourceNotFoundError,
35 LocalExecutorConfigDecodeError,
36 LocalFileSystemError,
37 LocalUnsupportedSourceKindError,
38 LocalWorkspaceStateDecodeError,
39 RuntimeLocalWorkspaceMismatchError,
40 RuntimeLocalWorkspaceUnavailableError,
41} from "./local-errors";
33import {42import {
34 requireRuntimeLocalWorkspace,43 requireRuntimeLocalWorkspace,
35} from "./local-runtime-context";44} from "./local-runtime-context";
@@ -231,7 +240,21 @@ const configAuthFromSource = (input: { · 195 unmodified lines
231 return input.existingConfigAuth;240 return input.existingConfigAuth;
232};241};
233 242
234const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) =>243const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId): Effect.Effect<{
244 context: ResolvedLocalWorkspaceContext;
245 installation: {
246 workspaceId: WorkspaceId;
247 accountId: AccountId;
248 };
249 loadedConfig: LoadedLocalExecutorConfig;
250 workspaceState: LocalWorkspaceState;
251},
252 | RuntimeLocalWorkspaceUnavailableError
253 | RuntimeLocalWorkspaceMismatchError
254 | LocalFileSystemError
255 | LocalExecutorConfigDecodeError
256 | LocalWorkspaceStateDecodeError,
257never> =>
235 Effect.gen(function* () {258 Effect.gen(function* () {
236 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);259 const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId);
237 const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context);260 const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context);
@@ -256,11 +279,16 @@ const buildLocalSourceRecord = (input: { · 18 unmodified lines
256}): Effect.Effect<{279}): Effect.Effect<{
257 source: Source;280 source: Source;
258 sourceId: SourceId;281 sourceId: SourceId;
259}, Error, never> =>282}, LocalConfiguredSourceNotFoundError | Error, never> =>
260 Effect.gen(function* () {283 Effect.gen(function* () {
261 const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId];284 const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId];
262 if (!sourceConfig) {285 if (!sourceConfig) {
263 return yield* Effect.fail(new Error(`Configured source not found for id ${input.sourceId}`));286 return yield* Effect.fail(
287 new LocalConfiguredSourceNotFoundError({
288 message: `Configured source not found for id ${input.sourceId}`,
289 sourceId: input.sourceId,
290 }),
291 );
264 }292 }
265 293
266 const existingState = input.workspaceState.sources[input.sourceId];294 const existingState = input.workspaceState.sources[input.sourceId];
@@ -356,7 +384,17 @@ export const loadSourcesInWorkspace = ( · 89 unmodified lines
356 options: {384 options: {
357 actorAccountId?: AccountId | null;385 actorAccountId?: AccountId | null;
358 } = {},386 } = {},
359) =>387): Effect.Effect<
388 readonly Source[],
389 | RuntimeLocalWorkspaceUnavailableError
390 | RuntimeLocalWorkspaceMismatchError
391 | LocalFileSystemError
392 | LocalExecutorConfigDecodeError
393 | LocalWorkspaceStateDecodeError
394 | LocalConfiguredSourceNotFoundError
395 | Error,
396 never
397> =>
360 Effect.gen(function* () {398 Effect.gen(function* () {
361 const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);399 const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId);
362 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);400 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);
@@ -382,13 +420,26 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 19 unmodified lines
382 workspaceId: WorkspaceId;420 workspaceId: WorkspaceId;
383 sourceId: Source["id"];421 sourceId: Source["id"];
384 actorAccountId?: AccountId | null;422 actorAccountId?: AccountId | null;
385}) =>423}): Effect.Effect<
424 Source,
425 | RuntimeLocalWorkspaceUnavailableError
426 | RuntimeLocalWorkspaceMismatchError
427 | LocalFileSystemError
428 | LocalExecutorConfigDecodeError
429 | LocalWorkspaceStateDecodeError
430 | LocalConfiguredSourceNotFoundError
431 | Error,
432 never
433> =>
386 Effect.gen(function* () {434 Effect.gen(function* () {
387 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);435 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
388 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);436 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);
389 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {437 if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
390 return yield* Effect.fail(438 return yield* Effect.fail(
391 new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),439 new LocalConfiguredSourceNotFoundError({
440 message: `Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
441 sourceId: input.sourceId,
442 }),
392 );443 );
393 }444 }
394 445
@@ -456,7 +507,10 @@ const configSourceFromLocalSource = (input: { · 61 unmodified lines
456 binding: cloneJson(input.source.binding) as Extract<LocalConfigSource, { kind: "mcp" }>["binding"],507 binding: cloneJson(input.source.binding) as Extract<LocalConfigSource, { kind: "mcp" }>["binding"],
457 };508 };
458 default:509 default:
459 throw new Error(`Unsupported source kind for local config: ${input.source.kind}`);510 throw new LocalUnsupportedSourceKindError({
511 message: `Unsupported source kind for local config: ${input.source.kind}`,
512 kind: input.source.kind,
513 });
460 }514 }
461};515};
462 516