diff --git a/.changeset/storybook-real-optimizer.md b/.changeset/storybook-real-optimizer.md new file mode 100644 index 00000000000..55e2657feba --- /dev/null +++ b/.changeset/storybook-real-optimizer.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Storybook gains a "With real optimizer" story: the full editor, built from source with fast refresh, running optimization studies against a local Petrinaut Optimizer service. Start it with `yarn dev:petrinaut-optimization --storybook`. diff --git a/apps/petrinaut-website/package.json b/apps/petrinaut-website/package.json index 46e48eeb96f..7412c7896f4 100644 --- a/apps/petrinaut-website/package.json +++ b/apps/petrinaut-website/package.json @@ -13,7 +13,7 @@ "lint:eslint": "oxlint --type-aware --report-unused-disable-directives-severity=error .", "lint:tsc": "tsgo --noEmit", "preview": "vite preview", - "test:unit": "vitest run" + "test:unit": "vitest run --passWithNoTests" }, "dependencies": { "@ai-sdk/openai": "3.0.63", diff --git a/apps/petrinaut-website/scripts/optimization-dev.mjs b/apps/petrinaut-website/scripts/optimization-dev.mjs index f2c030ba372..7c60420ee91 100644 --- a/apps/petrinaut-website/scripts/optimization-dev.mjs +++ b/apps/petrinaut-website/scripts/optimization-dev.mjs @@ -15,6 +15,15 @@ const container = "petrinaut-opt-website-dev"; // nosemgrep: typescript.react.security.react-insecure-request.react-insecure-request const optimizerOrigin = "http://127.0.0.1:4004"; +// `--storybook` starts Petrinaut's Storybook (editor built from source, with +// fast refresh) against the optimizer instead of the demo website (which +// consumes the built dist). Remaining arguments go to the spawned dev server. +const cliArguments = process.argv.slice(2); +const storybookMode = cliArguments.includes("--storybook"); +const forwardedArguments = cliArguments.filter( + (argument) => argument !== "--storybook", +); + const wait = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs)); @@ -166,21 +175,40 @@ try { await waitForOptimizer(); } - console.log("Building Petrinaut for the demo website..."); - await run("turbo", ["build", "--filter", "@hashintel/petrinaut"]); - - console.log("Starting the Petrinaut optimization demo..."); - // Extra arguments go to Vite, so a caller can pin the port: - // `yarn dev:petrinaut-optimization --port 5175 --strictPort`. - websiteProcess = spawn("yarn", ["vite", ...process.argv.slice(2)], { - cwd: appDirectory, - env: { - ...process.env, - PETRINAUT_OPT_ORIGIN: optimizerOrigin, - VITE_PETRINAUT_OPT_PROVIDER: "service", - }, - stdio: "inherit", - }); + const providerEnv = { + ...process.env, + PETRINAUT_OPT_ORIGIN: optimizerOrigin, + VITE_PETRINAUT_OPT_PROVIDER: "service", + }; + + if (storybookMode) { + console.log("Starting Petrinaut's Storybook against the optimizer..."); + // Through Turborepo so Storybook's workspace dependencies are built; + // Storybook itself serves the editor from source with fast refresh. + websiteProcess = spawn( + "turbo", + [ + "run", + "dev", + "--filter", + "@hashintel/petrinaut", + ...(forwardedArguments.length > 0 ? ["--", ...forwardedArguments] : []), + ], + { cwd: repositoryRoot, env: providerEnv, stdio: "inherit" }, + ); + } else { + console.log("Building Petrinaut for the demo website..."); + await run("turbo", ["build", "--filter", "@hashintel/petrinaut"]); + + console.log("Starting the Petrinaut optimization demo..."); + // Extra arguments go to Vite, so a caller can pin the port: + // `yarn dev:petrinaut-optimization --port 5175 --strictPort`. + websiteProcess = spawn("yarn", ["vite", ...forwardedArguments], { + cwd: appDirectory, + env: providerEnv, + stdio: "inherit", + }); + } const forwardSignal = (signal) => websiteProcess?.kill(signal); const handleSigint = () => forwardSignal("SIGINT"); diff --git a/apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.ts b/apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.ts index a8b959da96b..e2298f3ee34 100644 --- a/apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.ts +++ b/apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.ts @@ -1,14 +1,6 @@ -import { - attachPetrinautOptimizationRunStream, - createPetrinautOptimizerClient, - PetrinautOptimizerHttpError, - petrinautOptimizerHttpErrorFromResponse, -} from "@local/petrinaut-optimizer-client"; +import { createServicePetrinautOptimization } from "@local/petrinaut-optimizer-client"; -import type { - PetrinautOptimization, - PetrinautOptimizationEvent, -} from "@hashintel/petrinaut-core"; +import type { PetrinautOptimization } from "@hashintel/petrinaut-core"; import type { PetrinautOptimizerFetch } from "@local/petrinaut-optimizer-client"; /** @@ -23,103 +15,11 @@ const petrinautOptEndpoint = (): URL => typeof location === "undefined" ? "http://localhost/" : location.href, ); -/** - * Stamp the duck-typed classification fields Petrinaut's optimization - * provider reads (`category`, `httpStatus`, `retryAfter`) onto the client's - * HTTP error, so e.g. a 404 on re-attaching to an expired run silently drops - * the record instead of surfacing a raw error message. - */ -const classifyHttpError = (error: unknown): unknown => - error instanceof PetrinautOptimizerHttpError - ? Object.assign(error, { - category: "http", - httpStatus: error.status, - ...(error.retryAfter === null - ? {} - : { retryAfter: Number.parseInt(error.retryAfter, 10) }), - }) - : error; - -/** - * Classify a mid-stream failure so the provider reconnects with its cursor - * instead of failing the run on the first dropped connection. Aborts pass - * through untouched. A response body that dies mid-stream rejects the reader - * with a `TypeError`, which is a transport failure rather than a malformed - * frame — the remaining non-abort errors are the decoder's own validation - * failures, which stay `protocol`. - */ -const classifyStreamError = (error: unknown): unknown => - error instanceof Error && error.name !== "AbortError" - ? Object.assign(error, { - category: error instanceof TypeError ? "network" : "protocol", - }) - : error; - -/** - * Classify a request-time failure: HTTP errors keep their status semantics, - * and anything else non-abort (a fetch `TypeError` from a dropped - * connection) is `network` — so an attach that dies before responding - * reconnects with backoff exactly like a mid-stream drop, instead of - * definitively failing a possibly-live run. - */ -const classifyRequestError = (error: unknown): unknown => - error instanceof PetrinautOptimizerHttpError - ? classifyHttpError(error) - : error instanceof Error && error.name !== "AbortError" - ? Object.assign(error, { category: "network" }) - : error; - /** Create the local-only Petrinaut capability backed directly by Python. */ export const createPetrinautOptOptimization = ( fetchImpl: PetrinautOptimizerFetch = fetch, -): PetrinautOptimization => { - const client = createPetrinautOptimizerClient( - petrinautOptEndpoint(), +): PetrinautOptimization => + createServicePetrinautOptimization({ + endpoint: petrinautOptEndpoint, fetchImpl, - ); - // openapi-fetch names its verb methods in caps; alias them so call sites - // don't read as constructor calls (oxlint's new-cap). - const { DELETE: deleteRun, POST: postRun } = client; - - return { - async createOptimizationRun(input, options) { - const created = await postRun("/optimize/runs", { - body: input, - ...(options?.signal ? { signal: options.signal as AbortSignal } : {}), - }).catch((error: unknown) => { - throw classifyRequestError(error); - }); - if (!created.response.ok || !created.data?.run_id) { - throw classifyHttpError( - await petrinautOptimizerHttpErrorFromResponse(created.response), - ); - } - return { runId: created.data.run_id }; - }, - async *attachOptimizationRun(runId, options) { - let events: AsyncIterable; - try { - ({ events } = await attachPetrinautOptimizationRunStream({ - endpoint: petrinautOptEndpoint(), - fetchImpl, - runId, - ...(options?.cursor === undefined ? {} : { cursor: options.cursor }), - ...(options?.signal ? { signal: options.signal } : {}), - })); - } catch (error) { - throw classifyRequestError(error); - } - options?.onAttached?.(); - try { - yield* events; - } catch (error) { - throw classifyStreamError(error); - } - }, - async cancelOptimizationRun(runId) { - await deleteRun("/optimize/runs/{run_id}", { - params: { path: { run_id: runId } }, - }); - }, - }; -}; + }); diff --git a/libs/@hashintel/petrinaut/.oxlintrc.json b/libs/@hashintel/petrinaut/.oxlintrc.json index 58d867414e8..86f12a658f4 100644 --- a/libs/@hashintel/petrinaut/.oxlintrc.json +++ b/libs/@hashintel/petrinaut/.oxlintrc.json @@ -11,10 +11,21 @@ "browser": true }, "rules": { - "array-callback-return": ["error", { "allowImplicit": true }], + "array-callback-return": [ + "error", + { + "allowImplicit": true + } + ], "default-case-last": "error", "default-param-last": "error", - "eqeqeq": ["error", "always", { "null": "ignore" }], + "eqeqeq": [ + "error", + "always", + { + "null": "ignore" + } + ], "guard-for-in": "error", "no-alert": "error", "no-cond-assign": ["error", "always"], @@ -34,7 +45,9 @@ "no-template-curly-in-string": "error", "no-unsafe-optional-chaining": [ "error", - { "disallowArithmeticOperators": true } + { + "disallowArithmeticOperators": true + } ], "no-unused-vars": [ "error", @@ -44,19 +57,28 @@ "varsIgnorePattern": "^_+" } ], - "no-void": ["error", { "allowAsStatement": true }], - + "no-void": [ + "error", + { + "allowAsStatement": true + } + ], "no-console": "error", "new-cap": "error", "no-new-func": "error", "func-names": "error", "no-bitwise": "error", "no-multi-assign": "error", - "no-restricted-globals": [ "error", - { "name": "isFinite", "message": "Use Number.isFinite instead" }, - { "name": "isNaN", "message": "Use Number.isNaN instead" }, + { + "name": "isFinite", + "message": "Use Number.isFinite instead" + }, + { + "name": "isNaN", + "message": "Use Number.isNaN instead" + }, "event", "name", "length", @@ -64,7 +86,6 @@ ], "no-shadow": "error", "no-use-before-define": "error", - "no-restricted-imports": [ "error", { @@ -76,7 +97,6 @@ ] } ], - "import/no-named-as-default": "error", "import/no-named-as-default-member": "error", "import/no-mutable-exports": "error", @@ -84,15 +104,28 @@ "import/no-named-default": "error", "import/no-self-import": "error", "import/no-cycle": "error", - - "react/jsx-pascal-case": ["error", { "allowAllCaps": true }], + "react/jsx-pascal-case": [ + "error", + { + "allowAllCaps": true + } + ], "react/no-danger": "error", - "react/jsx-no-target-blank": ["error", { "enforceDynamicLinks": "always" }], + "react/jsx-no-target-blank": [ + "error", + { + "enforceDynamicLinks": "always" + } + ], "react/jsx-no-comment-textnodes": "error", "react/no-array-index-key": "error", "react/button-has-type": [ "error", - { "button": true, "submit": true, "reset": false } + { + "button": true, + "submit": true, + "reset": false + } ], "react-hooks-js/static-components": "error", "react-hooks-js/use-memo": "error", @@ -110,12 +143,19 @@ "react-hooks-js/unsupported-syntax": "error", "react-hooks-js/config": "error", "react-hooks-js/gating": "error", - "jsx-a11y/prefer-tag-over-role": "off", - "jsx-a11y/aria-role": ["error", { "ignoreNonDOM": false }], + "jsx-a11y/aria-role": [ + "error", + { + "ignoreNonDOM": false + } + ], "jsx-a11y/no-noninteractive-tabindex": [ "error", - { "tags": [], "roles": ["tabpanel"] } + { + "tags": [], + "roles": ["tabpanel"] + } ], "jsx-a11y/label-has-associated-control": "error", "jsx-a11y/no-static-element-interactions": [ @@ -131,7 +171,6 @@ ] } ], - "@typescript-eslint/ban-ts-comment": [ "error", { @@ -149,10 +188,8 @@ "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-function-type": "error", - "unicorn/no-new-array": "off", "unicorn/filename-case": "error", - "constructor-super": "off", "no-class-assign": "off", "no-const-assign": "off", @@ -175,5 +212,13 @@ "*.gen.*", "*.tsbuildinfo", ".turbo/**" + ], + "overrides": [ + { + "files": ["src/**/*.stories.tsx", ".storybook/**/*.{ts,tsx}"], + "rules": { + "no-restricted-imports": "off" + } + } ] } diff --git a/libs/@hashintel/petrinaut/.storybook/main.ts b/libs/@hashintel/petrinaut/.storybook/main.ts index a8392f9c1f9..d8c9cf8cbb3 100644 --- a/libs/@hashintel/petrinaut/.storybook/main.ts +++ b/libs/@hashintel/petrinaut/.storybook/main.ts @@ -4,6 +4,23 @@ const config: StorybookConfig = { stories: ["../src/**/*.stories.@(ts|tsx)"], framework: "@storybook/react-vite", staticDirs: ["../public"], + // Same dev proxy as the demo website, so the "With real optimizer" story + // can reach a locally running Petrinaut Optimizer without CORS changes to + // the service. Harmless when nothing serves the target. + viteFinal: (viteConfig) => ({ + ...viteConfig, + server: { + ...viteConfig.server, + proxy: { + ...viteConfig.server?.proxy, + "/api/petrinaut-opt": { + target: process.env.PETRINAUT_OPT_ORIGIN ?? "http://127.0.0.1:4004", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/petrinaut-opt/u, ""), + }, + }, + }, + }), }; export default config; diff --git a/libs/@hashintel/petrinaut/package.json b/libs/@hashintel/petrinaut/package.json index 416a670e60b..e1a0d405560 100644 --- a/libs/@hashintel/petrinaut/package.json +++ b/libs/@hashintel/petrinaut/package.json @@ -84,6 +84,7 @@ }, "devDependencies": { "@hashintel/ds-helpers": "workspace:*", + "@local/petrinaut-optimizer-client": "workspace:*", "@pandacss/dev": "1.11.1", "@rolldown/plugin-babel": "0.2.1", "@storybook/react-vite": "10.2.19", diff --git a/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx b/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx index f4840dc798d..9314e6606ff 100644 --- a/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx +++ b/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx @@ -1,7 +1,11 @@ import { useMemo, useState, useEffect } from "react"; import { css } from "@hashintel/ds-helpers/css"; -import { sirModel } from "@hashintel/petrinaut-core/examples"; +import { + sirModel, + supplyChainProfit, +} from "@hashintel/petrinaut-core/examples"; +import { createServicePetrinautOptimization } from "@local/petrinaut-optimizer-client"; import { createJsonDocHandle, @@ -9,6 +13,7 @@ import { type PetrinautHandleCapabilities, type SDCPN, } from "../main"; +import { PetrinautOptimizationContext } from "../react/optimization-context"; import { Petrinaut } from "../ui/petrinaut"; import { PetrinautStoryProvider } from "./petrinaut-story-provider"; import { createStorybookAiTransport } from "./views/Editor/panels/create-storybook-ai-transport"; @@ -452,6 +457,54 @@ export const Default: Story = { ), }; +/** + * Whether the host serves a real Petrinaut Optimizer behind the Storybook + * dev proxy. `yarn dev:petrinaut-optimization --storybook` sets it. + */ +const realOptimizerEnabled = + (import.meta as { env?: Record }).env?.[ + "VITE_PETRINAUT_OPT_PROVIDER" + ] === "service"; + +const realOptimizerGuidanceStyle = css({ + padding: "6", + fontSize: "sm", + color: "neutral.s100", + maxWidth: "[60ch]", +}); + +/** + * The full editor against the real optimizer service, with the editor built + * from source — the fast-refresh counterpart of the demo website's + * `/optimization` route. Optimization studies created in Simulate mode run + * on the local Petrinaut Optimizer container. + */ +export const WithRealOptimizer: Story = { + render: () => + realOptimizerEnabled ? ( + new URL("/api/petrinaut-opt/", location.href), + })} + > +
+ +
+
+ ) : ( +
+ This story talks to a real Petrinaut Optimizer and is inactive: start + Storybook through{" "} + yarn dev:petrinaut-optimization --storybook from the + repository root, which runs the optimizer container and sets{" "} + VITE_PETRINAUT_OPT_PROVIDER=service. +
+ ), +}; + export const Readonly: Story = { render: () => (
diff --git a/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx b/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx index a5e02821cc0..dcbc6935e2a 100644 --- a/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx +++ b/libs/@local/petrinaut-arch-docs/content/optimizer/running-the-loop-locally.mdx @@ -44,10 +44,30 @@ does six things in order: optimizer is left alone. Docker must be running unless a reusable optimizer is already serving. -Extra arguments are forwarded to Vite, so +Extra arguments are forwarded to the dev server, so `yarn dev:petrinaut-optimization --port 5175 --strictPort` pins the website port for tooling that needs to know it. +## With Storybook, for editor work + +The website consumes Petrinaut's **built dist**, so editing the editor means +rebuilding it. Storybook builds the editor from source with fast refresh, and +can run against the same real optimizer: + +```sh +yarn dev:petrinaut-optimization --storybook +``` + +The container lifecycle is identical (steps 1–3 and 6); instead of the +website, it starts Petrinaut's Storybook through Turborepo with the provider +environment set. Open the **Petrinaut → With real optimizer** story: the full +editor on a net with an objective, where optimization studies created in +Simulate mode run on the local service. Storybook's dev server carries the +same `/api/petrinaut-opt/*` proxy as the website's, and the story explains +how to launch when the environment is missing. Remaining arguments go to the +Storybook script (`yarn dev:petrinaut-optimization --storybook -- --port 6007` +through Turborepo's pass-through). + ## Why plain `yarn dev` shows no Optimizations view The editor renders the Optimizations tab only when a diff --git a/libs/@local/petrinaut-optimizer-client/src/index.ts b/libs/@local/petrinaut-optimizer-client/src/index.ts index fea8d04224d..4de1dce9b49 100644 --- a/libs/@local/petrinaut-optimizer-client/src/index.ts +++ b/libs/@local/petrinaut-optimizer-client/src/index.ts @@ -13,5 +13,6 @@ export { PetrinautOptimizerHttpError, petrinautOptimizerHttpErrorFromResponse, } from "./optimizer-http.js"; +export { createServicePetrinautOptimization } from "./service-optimization.js"; export type { PetrinautOptimizerFetch } from "./optimizer-http.js"; export type { components, operations, paths, webhooks } from "./openapi.gen.js"; diff --git a/apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.test.ts b/libs/@local/petrinaut-optimizer-client/src/service-optimization.test.ts similarity index 84% rename from apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.test.ts rename to libs/@local/petrinaut-optimizer-client/src/service-optimization.test.ts index 996541b48d3..91815fc3c8e 100644 --- a/apps/petrinaut-website/src/main/app/optimization-demo/petrinaut-opt-optimization.test.ts +++ b/libs/@local/petrinaut-optimizer-client/src/service-optimization.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { createPetrinautOptOptimization } from "./petrinaut-opt-optimization"; +import { createServicePetrinautOptimization } from "./service-optimization.js"; import type { PetrinautOptimizationInput } from "@hashintel/petrinaut-core"; @@ -9,8 +9,10 @@ const input = { study: { trials: 2 }, } as PetrinautOptimizationInput; -describe("createPetrinautOptOptimization", () => { - it("creates, attaches to, and cancels runs via the development proxy", async () => { +const endpoint = () => new URL("/api/petrinaut-opt/", "http://localhost/"); + +describe("createServicePetrinautOptimization", () => { + it("creates, attaches to, and cancels runs via the endpoint", async () => { const fetchImpl = vi.fn(async (_url: string | URL, init?: RequestInit) => { if (init?.method === "POST") { return Promise.resolve( @@ -30,7 +32,10 @@ describe("createPetrinautOptOptimization", () => { }), ); }); - const optimization = createPetrinautOptOptimization(fetchImpl); + const optimization = createServicePetrinautOptimization({ + endpoint, + fetchImpl, + }); const { runId } = await optimization.createOptimizationRun(input); expect(runId).toBe("run-1"); @@ -69,7 +74,10 @@ describe("createPetrinautOptOptimization", () => { ), ), ); - const optimization = createPetrinautOptOptimization(fetchImpl); + const optimization = createServicePetrinautOptimization({ + endpoint, + fetchImpl, + }); const onAttached = vi.fn(); const error = await (async () => { diff --git a/libs/@local/petrinaut-optimizer-client/src/service-optimization.ts b/libs/@local/petrinaut-optimizer-client/src/service-optimization.ts new file mode 100644 index 00000000000..d0614638d96 --- /dev/null +++ b/libs/@local/petrinaut-optimizer-client/src/service-optimization.ts @@ -0,0 +1,126 @@ +/** + * The `PetrinautOptimization` capability backed by a Petrinaut Optimizer + * service, for hosts that talk to the service directly: the demo website and + * Petrinaut's Storybook. Beyond wiring the HTTP client, it stamps the + * duck-typed classification fields Petrinaut's optimization provider reads, + * so its reconnect logic treats transport failures as retryable. + */ +import { attachPetrinautOptimizationRunStream } from "./attach-optimization-run.js"; +import { createPetrinautOptimizerClient } from "./client.js"; +import { + PetrinautOptimizerHttpError, + petrinautOptimizerHttpErrorFromResponse, +} from "./optimizer-http.js"; + +import type { PetrinautOptimizerFetch } from "./optimizer-http.js"; +import type { + PetrinautOptimization, + PetrinautOptimizationEvent, +} from "@hashintel/petrinaut-core"; + +/** + * Stamp the duck-typed classification fields Petrinaut's optimization + * provider reads (`category`, `httpStatus`, `retryAfter`) onto the client's + * HTTP error, so e.g. a 404 on re-attaching to an expired run silently drops + * the record instead of surfacing a raw error message. + */ +const classifyHttpError = (error: unknown): unknown => + error instanceof PetrinautOptimizerHttpError + ? Object.assign(error, { + category: "http", + httpStatus: error.status, + ...(error.retryAfter === null + ? {} + : { retryAfter: Number.parseInt(error.retryAfter, 10) }), + }) + : error; + +/** + * Classify a mid-stream failure so the provider reconnects with its cursor + * instead of failing the run on the first dropped connection. Aborts pass + * through untouched. A response body that dies mid-stream rejects the reader + * with a `TypeError`, which is a transport failure rather than a malformed + * frame — the remaining non-abort errors are the decoder's own validation + * failures, which stay `protocol`. + */ +const classifyStreamError = (error: unknown): unknown => + error instanceof Error && error.name !== "AbortError" + ? Object.assign(error, { + category: error instanceof TypeError ? "network" : "protocol", + }) + : error; + +/** + * Classify a request-time failure: HTTP errors keep their status semantics, + * and anything else non-abort (a fetch `TypeError` from a dropped + * connection) is `network` — so an attach that dies before responding + * reconnects with backoff exactly like a mid-stream drop, instead of + * definitively failing a possibly-live run. + */ +const classifyRequestError = (error: unknown): unknown => + error instanceof PetrinautOptimizerHttpError + ? classifyHttpError(error) + : error instanceof Error && error.name !== "AbortError" + ? Object.assign(error, { category: "network" }) + : error; + +/** + * Create the capability against the service at `endpoint`. The endpoint is a + * function because hosts resolve it against the current document (a dev + * proxy prefix such as `/api/petrinaut-opt/`), which is only known at call + * time. + */ +export const createServicePetrinautOptimization = ({ + endpoint, + fetchImpl = fetch, +}: { + endpoint: () => URL; + fetchImpl?: PetrinautOptimizerFetch; +}): PetrinautOptimization => { + const client = createPetrinautOptimizerClient(endpoint(), fetchImpl); + // openapi-fetch names its verb methods in caps; alias them so call sites + // don't read as constructor calls (oxlint's new-cap). + const { DELETE: deleteRun, POST: postRun } = client; + + return { + async createOptimizationRun(input, options) { + const created = await postRun("/optimize/runs", { + body: input, + ...(options?.signal ? { signal: options.signal as AbortSignal } : {}), + }).catch((error: unknown) => { + throw classifyRequestError(error); + }); + if (!created.response.ok || !created.data?.run_id) { + throw classifyHttpError( + await petrinautOptimizerHttpErrorFromResponse(created.response), + ); + } + return { runId: created.data.run_id }; + }, + async *attachOptimizationRun(runId, options) { + let events: AsyncIterable; + try { + ({ events } = await attachPetrinautOptimizationRunStream({ + endpoint: endpoint(), + fetchImpl, + runId, + ...(options?.cursor === undefined ? {} : { cursor: options.cursor }), + ...(options?.signal ? { signal: options.signal } : {}), + })); + } catch (error) { + throw classifyRequestError(error); + } + options?.onAttached?.(); + try { + yield* events; + } catch (error) { + throw classifyStreamError(error); + } + }, + async cancelOptimizationRun(runId) { + await deleteRun("/optimize/runs/{run_id}", { + params: { path: { run_id: runId } }, + }); + }, + }; +}; diff --git a/yarn.lock b/yarn.lock index ae1735302d0..077dfb142f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7774,6 +7774,7 @@ __metadata: "@hashintel/ds-helpers": "workspace:*" "@hashintel/petrinaut-core": "workspace:^" "@hashintel/refractive": "workspace:^" + "@local/petrinaut-optimizer-client": "workspace:*" "@monaco-editor/react": "npm:4.8.0-rc.3" "@pandacss/dev": "npm:1.11.1" "@rolldown/plugin-babel": "npm:0.2.1"