Skip to content

Commit 34ae828

Browse files
fix: feedback
- Fix TS2379/TS2375/TS2322 errors from `exactOptionalPropertyTypes` by normalizing zod-parsed error bodies into `SerializedError` and by conditionally spreading optional options. - Cast `retryPolicy` to `RetryPolicy` and step-attempt `context` to `StepAttemptContext | null` at the HTTP boundary. - Work around TS2589 deep-instantiation in Hono's `JSONParsed<T>` by routing complex response bodies through a `jsonResponse` helper and splitting step-attempt routes across sub-apps. - Re-export `RetryPolicy`, `SerializedError`, and `StepAttemptContext` from `openworkflow/internal` so the server package can reference them. - Drop unused `BACKEND_ERROR_CODES` and `HttpValidationError` exports, and deduplicate the worker-lease schemas to satisfy knip. - Reformat server sources with prettier.
1 parent ad76d8c commit 34ae828

16 files changed

Lines changed: 350 additions & 525 deletions

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"**/openworkflow.config.*",
2020
"openworkflow/**/*.ts",
2121
"packages/dashboard/src/components/ui/*.tsx",
22-
"packages/docs/style.css"
22+
"packages/docs/style.css",
23+
"packages/openworkflow/http.ts"
2324
],
2425
"ignoreIssues": {
2526
"packages/dashboard/src/components/ui/*.tsx": ["exports"],

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/commands.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
getConfigFileName,
66
getExampleWorkflowFileName,
77
getRunFileName,
8-
validateDashboardPort,
8+
validatePort,
99
} from "./commands.js";
1010
import fs from "node:fs";
1111
import os from "node:os";
@@ -164,27 +164,29 @@ describe("getDashboardSpawnOptions", () => {
164164
});
165165
});
166166

167-
describe("validateDashboardPort", () => {
167+
describe("validatePort", () => {
168168
test("returns undefined when no custom port is provided", () => {
169-
expect(validateDashboardPort()).toBeUndefined();
169+
expect(validatePort(undefined, "dashboard")).toBeUndefined();
170170
});
171171

172172
test("returns the port when it is within range", () => {
173-
expect(validateDashboardPort(3001)).toBe(3001);
173+
expect(validatePort(3001, "dashboard")).toBe(3001);
174174
});
175175

176176
test("throws for non-integer ports", () => {
177-
expect(() => validateDashboardPort(Number.NaN)).toThrow(
177+
expect(() => validatePort(Number.NaN, "dashboard")).toThrow(
178178
"Invalid dashboard port.",
179179
);
180-
expect(() => validateDashboardPort(3000.5)).toThrow(
180+
expect(() => validatePort(3000.5, "dashboard")).toThrow(
181181
"Invalid dashboard port.",
182182
);
183183
});
184184

185185
test("throws for out-of-range ports", () => {
186-
expect(() => validateDashboardPort(0)).toThrow("Invalid dashboard port.");
187-
expect(() => validateDashboardPort(65_536)).toThrow(
186+
expect(() => validatePort(0, "dashboard")).toThrow(
187+
"Invalid dashboard port.",
188+
);
189+
expect(() => validatePort(65_536, "dashboard")).toThrow(
188190
"Invalid dashboard port.",
189191
);
190192
});

packages/cli/commands.ts

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@ interface CommandOptions {
3535
config?: string;
3636
}
3737

38-
interface DashboardOptions extends CommandOptions {
39-
port?: number;
40-
}
41-
42-
interface ServerStartOptions extends CommandOptions {
38+
interface PortedOptions extends CommandOptions {
4339
port?: number;
4440
}
4541

@@ -362,19 +358,23 @@ export function getDashboardSpawnOptions(port?: number): {
362358
}
363359

364360
/**
365-
* Validate dashboard port option.
366-
* @param port - Optional dashboard port.
367-
* @returns Validated dashboard port.
368-
* @throws {CLIError} If the provided port is not an integer in the 1-65535 range.
361+
* Validate a port option.
362+
* @param port - Optional port number.
363+
* @param label - Label used in the error message (e.g. "dashboard", "server").
364+
* @returns Validated port, or undefined if not provided.
365+
* @throws {CLIError} If the port is not an integer in the 1-65535 range.
369366
*/
370-
export function validateDashboardPort(port?: number): number | undefined {
367+
export function validatePort(
368+
port: number | undefined,
369+
label: string,
370+
): number | undefined {
371371
if (port === undefined) {
372372
return undefined;
373373
}
374374

375375
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
376376
throw new CLIError(
377-
"Invalid dashboard port.",
377+
`Invalid ${label} port.`,
378378
"Use an integer between 1 and 65535, for example `--port 3001`.",
379379
);
380380
}
@@ -387,9 +387,9 @@ export function validateDashboardPort(port?: number): number | undefined {
387387
* @param options - Dashboard command options.
388388
* @returns Resolves when the dashboard process exits.
389389
*/
390-
export async function dashboard(options: DashboardOptions = {}): Promise<void> {
390+
export async function dashboard(options: PortedOptions = {}): Promise<void> {
391391
const configPath = options.config;
392-
const port = validateDashboardPort(options.port);
392+
const port = validatePort(options.port, "dashboard");
393393
consola.start("Starting dashboard...");
394394

395395
const { configFile } = await loadConfigWithEnv(configPath);
@@ -451,21 +451,15 @@ export async function dashboard(options: DashboardOptions = {}): Promise<void> {
451451
});
452452
}
453453

454-
export type { ServerStartOptions };
455-
456454
/**
457455
* openworkflow server start
458-
* Start the OpenWorkflow HTTP API server.
459456
* @param options - Server start options.
460457
*/
461-
export async function serverStart(
462-
options: ServerStartOptions = {},
463-
): Promise<void> {
464-
const { config: configPath, port: rawPort } = options;
465-
const port = rawPort ?? 3000;
458+
export async function serverStart(options: PortedOptions = {}): Promise<void> {
459+
const port = validatePort(options.port, "server") ?? 3000;
466460
consola.start("Starting server...");
467461

468-
const { configFile, config } = await loadConfigWithEnv(configPath);
462+
const { configFile, config } = await loadConfigWithEnv(options.config);
469463
if (!configFile) {
470464
throw new CLIError(
471465
"No config file found.",
@@ -474,32 +468,32 @@ export async function serverStart(
474468
}
475469
consola.info(`Using config: ${configFile}`);
476470

477-
let createServer: typeof import("@openworkflow/server").createServer;
478-
let serve: typeof import("@openworkflow/server").serve;
479-
try {
480-
({ createServer, serve } = await import("@openworkflow/server"));
481-
} catch {
482-
throw new CLIError(
483-
"@openworkflow/server is not installed.",
484-
'Run `npm install @openworkflow/server` to enable the "server start" command.',
485-
);
486-
}
487-
488471
const backend = config.backend;
489-
const server = createServer(backend, {
490-
logRequests: true,
491-
onError: (error, ctx) => {
492-
consola.error(`[${ctx.method} ${ctx.path}]`, error);
493-
},
494-
});
495-
const handle = serve(server, { port });
496-
consola.success(`Server listening on http://localhost:${String(port)}`);
497-
498-
registerGracefulShutdown({
472+
let handle: { close(): Promise<void> } | null = null;
473+
const gracefulShutdown = registerGracefulShutdown({
499474
noun: "server",
500-
stopApp: () => handle.close(),
475+
stopApp: async () => {
476+
await handle?.close();
477+
},
501478
backend,
502479
});
480+
481+
try {
482+
// still dynamic to defer hono's ~150KB until `server start` is invoked
483+
const { createServer, serve } = await import("@openworkflow/server");
484+
485+
const server = createServer(backend, {
486+
logRequests: true,
487+
onError: (error, ctx) => {
488+
consola.error(`[${ctx.method} ${ctx.path}]`, error);
489+
},
490+
});
491+
handle = serve(server, { port });
492+
consola.success(`Server listening on http://localhost:${String(port)}`);
493+
} catch (error) {
494+
await gracefulShutdown();
495+
throw error;
496+
}
503497
}
504498

505499
// -----------------------------------------------------------------------------
@@ -514,11 +508,10 @@ interface ShutdownOptions {
514508
}
515509

516510
/**
517-
* Wire SIGINT/SIGTERM to a graceful shutdown. The HTTP handle / worker is
518-
* stopped first (so no new work starts), then the backend is stopped even if
519-
* the app-level close fails.
511+
* Wire SIGINT/SIGTERM to a graceful shutdown. `stopApp` runs first; the
512+
* backend is stopped even if `stopApp` throws.
520513
* @param options - What to stop on shutdown
521-
* @returns The shutdown function (also registered against SIGINT/SIGTERM)
514+
* @returns The shutdown function
522515
*/
523516
function registerGracefulShutdown(
524517
options: ShutdownOptions,

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@
2828
},
2929
"dependencies": {
3030
"@clack/prompts": "^1.2.0",
31+
"@openworkflow/server": "*",
3132
"commander": "^14.0.3",
3233
"consola": "^3.4.2",
3334
"dotenv": "^17.4.2",
3435
"jiti": "^2.6.1",
3536
"nypm": "^0.6.5"
3637
},
3738
"devDependencies": {
38-
"@openworkflow/server": "*",
3939
"openworkflow": "*",
4040
"vitest": "^4.0.18"
4141
},

packages/openworkflow/core/error.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,17 @@ export interface SerializedError {
77
[key: string]: JsonValue;
88
}
99

10-
/**
11-
* Runtime tuple of every known Backend error code. Single source of truth —
12-
* the `BackendErrorCode` type is derived from this list so adding a code is
13-
* one edit.
14-
*/
15-
export const BACKEND_ERROR_CODES = ["NOT_FOUND", "CONFLICT"] as const;
10+
export type BackendErrorCode = "NOT_FOUND" | "CONFLICT";
1611

1712
/**
18-
* Error codes for typed Backend errors that can be mapped to HTTP status codes.
19-
*/
20-
export type BackendErrorCode = (typeof BACKEND_ERROR_CODES)[number];
21-
22-
/**
23-
* Type guard narrowing an arbitrary string to a known {@link BackendErrorCode}.
24-
* @param code - Candidate code string (e.g. from a server response)
25-
* @returns Whether `code` is a recognized backend error code
13+
* Type guard for {@link BackendErrorCode}.
14+
* @param code - The string to test
15+
* @returns True if `code` is a known backend error code
2616
*/
2717
export function isBackendErrorCode(code: string): code is BackendErrorCode {
28-
return (BACKEND_ERROR_CODES as readonly string[]).includes(code);
18+
return code === "NOT_FOUND" || code === "CONFLICT";
2919
}
3020

31-
/**
32-
* A typed error thrown by Backend implementations to signal well-known failure
33-
* modes. Consumers (e.g. the HTTP server) can inspect `code` to choose the
34-
* appropriate response status.
35-
*/
3621
// eslint-disable-next-line functional/no-classes, functional/no-class-inheritance
3722
export class BackendError extends Error {
3823
readonly code: BackendErrorCode;

packages/openworkflow/core/step-attempt.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import type { JsonValue } from "./json.js";
44
import type { Result } from "./result.js";
55
import { err, ok } from "./result.js";
66

7-
/**
8-
* Runtime tuple of every known step kind. Single source of truth — the
9-
* `StepKind` type is derived from this list so adding a kind is one edit.
10-
*/
7+
/** Runtime tuple of step kinds; {@link StepKind} is derived from it. */
118
export const STEP_KINDS = [
129
"function",
1310
"sleep",

packages/openworkflow/http.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BackendHttp, type BackendHttpOptions } from "./http/backend.js";

0 commit comments

Comments
 (0)