From 4a377a7d00f865526f92ed4e11fdfcb91bcb1861 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 30 Oct 2025 02:59:30 -0700 Subject: [PATCH 01/20] use ComponentApi --- example/convex/_generated/api.d.ts | 30 +- src/client/index.ts | 6 +- src/client/types.ts | 44 +-- src/component/_generated/api.d.ts | 424 ++--------------------------- 4 files changed, 51 insertions(+), 453 deletions(-) diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 02f0590..9a9b4cf 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -21,14 +21,6 @@ import type { FunctionReference, } from "convex/server"; -/** - * A utility for referencing Convex functions in your app's API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ declare const fullApi: ApiFromModules<{ admin: typeof admin; example: typeof example; @@ -37,14 +29,30 @@ declare const fullApi: ApiFromModules<{ transcription: typeof transcription; userConfirmation: typeof userConfirmation; }>; -declare const fullApiWithMounts: typeof fullApi; +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ export declare const api: FilterApi< - typeof fullApiWithMounts, + typeof fullApi, FunctionReference >; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ export declare const internal: FilterApi< - typeof fullApiWithMounts, + typeof fullApi, FunctionReference >; diff --git a/src/client/index.ts b/src/client/index.ts index 1a69e2a..a0463d8 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -31,7 +31,7 @@ import type { WorkflowStep, } from "../types.js"; import { safeFunctionName } from "./safeFunctionName.js"; -import type { OpaqueIds, WorkflowComponent } from "./types.js"; +import type { IdsToStrings, WorkflowComponent } from "./types.js"; import type { WorkflowCtx } from "./workflowContext.js"; import { workflowMutation } from "./workflowMutation.js"; @@ -88,7 +88,7 @@ export type WorkflowDefinition< }; export type WorkflowStatus = - | { type: "inProgress"; running: OpaqueIds[] } + | { type: "inProgress"; running: IdsToStrings[] } | { type: "completed"; result: unknown } | { type: "canceled" } | { type: "failed"; error: string }; @@ -191,7 +191,7 @@ export class WorkflowManager { this.component.workflow.getStatus, { workflowId }, ); - const running = inProgress.map((entry) => entry.step); + const running = inProgress.map((entry) => entry.step as IdsToStrings); switch (workflow.runResult?.kind) { case undefined: return { type: "inProgress", running }; diff --git a/src/client/types.ts b/src/client/types.ts index f0f4279..7a85f92 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -1,38 +1,14 @@ import type { Expand, FunctionReference } from "convex/server"; -import type { api } from "../component/_generated/api.js"; -import type { GenericId } from "convex/values"; +import type { ComponentApi } from "../component/_generated/component.js"; +import type { GenericId, Value } from "convex/values"; -export type WorkflowComponent = UseApi; +export type WorkflowComponent = ComponentApi; -export type UseApi = Expand<{ - [mod in keyof API]: API[mod] extends FunctionReference< - infer FType, - "public", - infer FArgs, - infer FReturnType, - infer FComponentPath - > - ? FunctionReference< - FType, - "internal", - OpaqueIds, - OpaqueIds, - FComponentPath - > - : UseApi; -}>; - -export type OpaqueIds = - T extends GenericId +export type IdsToStrings = + T extends GenericId ? string - : T extends string - ? `${T}` extends T - ? T - : string - : T extends (infer U)[] - ? OpaqueIds[] - : T extends ArrayBuffer - ? ArrayBuffer - : T extends object - ? { [K in keyof T]: OpaqueIds } - : T; + : T extends (infer U)[] + ? IdsToStrings[] + : T extends Record + ? { [K in keyof T]: IdsToStrings } + : T; diff --git a/src/component/_generated/api.d.ts b/src/component/_generated/api.d.ts index 4a58fb0..58605fc 100644 --- a/src/component/_generated/api.d.ts +++ b/src/component/_generated/api.d.ts @@ -22,14 +22,6 @@ import type { FunctionReference, } from "convex/server"; -/** - * A utility for referencing Convex functions in your app's API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ declare const fullApi: ApiFromModules<{ event: typeof event; journal: typeof journal; @@ -39,408 +31,30 @@ declare const fullApi: ApiFromModules<{ utils: typeof utils; workflow: typeof workflow; }>; -export type Mounts = { - event: { - create: FunctionReference< - "mutation", - "public", - { name: string; workflowId: string }, - string - >; - send: FunctionReference< - "mutation", - "public", - { - eventId?: string; - name?: string; - result: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - workflowId?: string; - workpoolOptions?: { - defaultRetryBehavior?: { - base: number; - initialBackoffMs: number; - maxAttempts: number; - }; - logLevel?: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - maxParallelism?: number; - retryActionsByDefault?: boolean; - }; - }, - string - >; - }; - journal: { - load: FunctionReference< - "query", - "public", - { shortCircuit?: boolean; workflowId: string }, - { - blocked?: boolean; - journalEntries: Array<{ - _creationTime: number; - _id: string; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - stepNumber: number; - workflowId: string; - }>; - logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - ok: boolean; - workflow: { - _creationTime: number; - _id: string; - args: any; - generationNumber: number; - logLevel?: any; - name?: string; - onComplete?: { context?: any; fnHandle: string }; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt?: any; - state?: any; - workflowHandle: string; - }; - } - >; - startSteps: FunctionReference< - "mutation", - "public", - { - generationNumber: number; - steps: Array<{ - retry?: - | boolean - | { base: number; initialBackoffMs: number; maxAttempts: number }; - schedulerOptions?: { runAt?: number } | { runAfter?: number }; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - }>; - workflowId: string; - workpoolOptions?: { - defaultRetryBehavior?: { - base: number; - initialBackoffMs: number; - maxAttempts: number; - }; - logLevel?: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - maxParallelism?: number; - retryActionsByDefault?: boolean; - }; - }, - Array<{ - _creationTime: number; - _id: string; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - stepNumber: number; - workflowId: string; - }> - >; - }; - workflow: { - cancel: FunctionReference< - "mutation", - "public", - { workflowId: string }, - null - >; - cleanup: FunctionReference< - "mutation", - "public", - { workflowId: string }, - boolean - >; - complete: FunctionReference< - "mutation", - "public", - { - generationNumber: number; - runResult: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - workflowId: string; - }, - null - >; - create: FunctionReference< - "mutation", - "public", - { - maxParallelism?: number; - onComplete?: { context?: any; fnHandle: string }; - startAsync?: boolean; - workflowArgs: any; - workflowHandle: string; - workflowName: string; - }, - string - >; - getStatus: FunctionReference< - "query", - "public", - { workflowId: string }, - { - inProgress: Array<{ - _creationTime: number; - _id: string; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - stepNumber: number; - workflowId: string; - }>; - logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - workflow: { - _creationTime: number; - _id: string; - args: any; - generationNumber: number; - logLevel?: any; - name?: string; - onComplete?: { context?: any; fnHandle: string }; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt?: any; - state?: any; - workflowHandle: string; - }; - } - >; - listSteps: FunctionReference< - "query", - "public", - { - order: "asc" | "desc"; - paginationOpts: { - cursor: string | null; - endCursor?: string | null; - id?: number; - maximumBytesRead?: number; - maximumRowsRead?: number; - numItems: number; - }; - workflowId: string; - }, - { - continueCursor: string; - isDone: boolean; - page: Array<{ - args: any; - completedAt?: number; - eventId?: string; - kind: "function" | "workflow" | "event"; - name: string; - nestedWorkflowId?: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - stepId: string; - stepNumber: number; - workId?: string; - workflowId: string; - }>; - pageStatus?: "SplitRecommended" | "SplitRequired" | null; - splitCursor?: string | null; - } - >; - }; -}; -// For now fullApiWithMounts is only fullApi which provides -// jump-to-definition in component client code. -// Use Mounts for the same type without the inference. -declare const fullApiWithMounts: typeof fullApi; +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ export declare const api: FilterApi< - typeof fullApiWithMounts, + typeof fullApi, FunctionReference >; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ export declare const internal: FilterApi< - typeof fullApiWithMounts, + typeof fullApi, FunctionReference >; From 87c5030613274860146689a4d945c015eea362e5 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 30 Oct 2025 03:02:58 -0700 Subject: [PATCH 02/20] use watcher loop with codegen instead of live types --- example/convex/tsconfig.json | 8 +------- package.json | 8 +++----- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/example/convex/tsconfig.json b/example/convex/tsconfig.json index 4c79336..d3d31bf 100644 --- a/example/convex/tsconfig.json +++ b/example/convex/tsconfig.json @@ -17,13 +17,7 @@ "module": "ESNext", "moduleResolution": "Bundler", "isolatedModules": true, - "noEmit": true, - - /* This should only be used in this example. Real apps should not attempt - * to compile TypeScript because differences between tsconfig.json files can - * cause the code to be compiled differently. - */ - "customConditions": ["@convex-dev/component-source"] + "noEmit": true }, "include": ["./**/*"], "exclude": ["./_generated"] diff --git a/package.json b/package.json index 80898a2..90ea5e7 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,14 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "dev": "run-p -r 'dev:backend' 'dev:frontend' 'build:watch'", - "dev:backend": "convex dev --live-component-sources --typecheck-components", + "dev": "run-p -r 'dev:*'", + "dev:backend": "convex dev --typecheck-components", "dev:frontend": "cd example && vite --clearScreen false", + "dev:build": "npx chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npx convex codegen --component ./src/component && npm run build' --initial", "predev": "npm run dev:backend -- --until-success", "clean": "rm -rf dist tsconfig.build.tsbuildinfo", "build": "tsc --project ./tsconfig.build.json && npm run copy:dts", "copy:dts": "rsync -a --include='*/' --include='*.d.ts' --exclude='*' src/ dist/ || cpy 'src/**/*.d.ts' 'dist/' --parents", - "build:watch": "npx chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build' --initial", "typecheck": "tsc --noEmit && tsc -p example/convex", "lint": "eslint src && eslint example/convex", "all": "run-p -r 'dev:backend' 'dev:frontend' 'build:watch' 'test:watch'", @@ -40,13 +40,11 @@ "exports": { "./package.json": "./package.json", ".": { - "@convex-dev/component-source": "./src/client/index.ts", "types": "./dist/client/index.d.ts", "default": "./dist/client/index.js" }, "./test": "./src/test.ts", "./convex.config": { - "@convex-dev/component-source": "./src/component/convex.config.ts", "types": "./dist/component/convex.config.d.ts", "default": "./dist/component/convex.config.js" } From 1c8b214b6ce93e72e5260a3021f0af6859fe56a2 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 30 Oct 2025 03:21:03 -0700 Subject: [PATCH 03/20] use ComponentApi --- src/client/types.ts | 1 - src/component/_generated/component.ts | 425 ++++++++++++++++++++++++++ 2 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 src/component/_generated/component.ts diff --git a/src/client/types.ts b/src/client/types.ts index 7a85f92..334ec2e 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -1,4 +1,3 @@ -import type { Expand, FunctionReference } from "convex/server"; import type { ComponentApi } from "../component/_generated/component.js"; import type { GenericId, Value } from "convex/values"; diff --git a/src/component/_generated/component.ts b/src/component/_generated/component.ts new file mode 100644 index 0000000..328854f --- /dev/null +++ b/src/component/_generated/component.ts @@ -0,0 +1,425 @@ +/* eslint-disable */ +/** + * Generated `ComponentApi` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { FunctionReference } from "convex/server"; + +/** + * A utility for referencing a Convex component's exposed API. + * + * Useful when expecting a parameter like `components.myComponent`. + * Usage: + * ```ts + * async function myFunction(ctx: QueryCtx, component: ComponentApi) { + * return ctx.runQuery(component.someFile.someQuery, { ...args }); + * } + * ``` + */ +export type ComponentApi = + { + event: { + create: FunctionReference< + "mutation", + "internal", + { name: string; workflowId: string }, + string, + Name + >; + send: FunctionReference< + "mutation", + "internal", + { + eventId?: string; + name?: string; + result: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + workflowId?: string; + workpoolOptions?: { + defaultRetryBehavior?: { + base: number; + initialBackoffMs: number; + maxAttempts: number; + }; + logLevel?: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; + maxParallelism?: number; + retryActionsByDefault?: boolean; + }; + }, + string, + Name + >; + }; + journal: { + load: FunctionReference< + "query", + "internal", + { shortCircuit?: boolean; workflowId: string }, + { + blocked?: boolean; + journalEntries: Array<{ + _creationTime: number; + _id: string; + step: + | { + args: any; + argsSize: number; + completedAt?: number; + functionType: "query" | "mutation" | "action"; + handle: string; + inProgress: boolean; + kind?: "function"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workId?: string; + } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } + | { + args: { eventId?: string }; + argsSize: number; + completedAt?: number; + eventId?: string; + inProgress: boolean; + kind: "event"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + }; + stepNumber: number; + workflowId: string; + }>; + logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; + ok: boolean; + workflow: { + _creationTime: number; + _id: string; + args: any; + generationNumber: number; + logLevel?: any; + name?: string; + onComplete?: { context?: any; fnHandle: string }; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt?: any; + state?: any; + workflowHandle: string; + }; + }, + Name + >; + startSteps: FunctionReference< + "mutation", + "internal", + { + generationNumber: number; + steps: Array<{ + retry?: + | boolean + | { base: number; initialBackoffMs: number; maxAttempts: number }; + schedulerOptions?: { runAt?: number } | { runAfter?: number }; + step: + | { + args: any; + argsSize: number; + completedAt?: number; + functionType: "query" | "mutation" | "action"; + handle: string; + inProgress: boolean; + kind?: "function"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workId?: string; + } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } + | { + args: { eventId?: string }; + argsSize: number; + completedAt?: number; + eventId?: string; + inProgress: boolean; + kind: "event"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + }; + }>; + workflowId: string; + workpoolOptions?: { + defaultRetryBehavior?: { + base: number; + initialBackoffMs: number; + maxAttempts: number; + }; + logLevel?: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; + maxParallelism?: number; + retryActionsByDefault?: boolean; + }; + }, + Array<{ + _creationTime: number; + _id: string; + step: + | { + args: any; + argsSize: number; + completedAt?: number; + functionType: "query" | "mutation" | "action"; + handle: string; + inProgress: boolean; + kind?: "function"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workId?: string; + } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } + | { + args: { eventId?: string }; + argsSize: number; + completedAt?: number; + eventId?: string; + inProgress: boolean; + kind: "event"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + }; + stepNumber: number; + workflowId: string; + }>, + Name + >; + }; + workflow: { + cancel: FunctionReference< + "mutation", + "internal", + { workflowId: string }, + null, + Name + >; + cleanup: FunctionReference< + "mutation", + "internal", + { workflowId: string }, + boolean, + Name + >; + complete: FunctionReference< + "mutation", + "internal", + { + generationNumber: number; + runResult: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + workflowId: string; + }, + null, + Name + >; + create: FunctionReference< + "mutation", + "internal", + { + maxParallelism?: number; + onComplete?: { context?: any; fnHandle: string }; + startAsync?: boolean; + workflowArgs: any; + workflowHandle: string; + workflowName: string; + }, + string, + Name + >; + getStatus: FunctionReference< + "query", + "internal", + { workflowId: string }, + { + inProgress: Array<{ + _creationTime: number; + _id: string; + step: + | { + args: any; + argsSize: number; + completedAt?: number; + functionType: "query" | "mutation" | "action"; + handle: string; + inProgress: boolean; + kind?: "function"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workId?: string; + } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } + | { + args: { eventId?: string }; + argsSize: number; + completedAt?: number; + eventId?: string; + inProgress: boolean; + kind: "event"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + }; + stepNumber: number; + workflowId: string; + }>; + logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; + workflow: { + _creationTime: number; + _id: string; + args: any; + generationNumber: number; + logLevel?: any; + name?: string; + onComplete?: { context?: any; fnHandle: string }; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt?: any; + state?: any; + workflowHandle: string; + }; + }, + Name + >; + listSteps: FunctionReference< + "query", + "internal", + { + order: "asc" | "desc"; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + workflowId: string; + }, + { + continueCursor: string; + isDone: boolean; + page: Array<{ + args: any; + completedAt?: number; + eventId?: string; + kind: "function" | "workflow" | "event"; + name: string; + nestedWorkflowId?: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + stepId: string; + stepNumber: number; + workId?: string; + workflowId: string; + }>; + pageStatus?: "SplitRecommended" | "SplitRequired" | null; + splitCursor?: string | null; + }, + Name + >; + }; + }; From a5ab34f05c1e44b13b8285e9be5ed40265718567 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 4 Nov 2025 00:50:41 -0800 Subject: [PATCH 04/20] component api --- .gitignore | 2 - eslint.config.js | 86 ++++++++++++++++++++++++++++-------- example/convex/tsconfig.json | 15 +++---- package-lock.json | 12 ++--- package.json | 24 +++++----- src/test.ts | 6 +-- 6 files changed, 95 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index dbffb69..3a547cc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,6 @@ dist-ssr explorations node_modules .eslintcache -# components are libraries! -.package-lock.json # this is a package-json-redirect stub dir, see https://github.com/andrewbranch/example-subpath-exports-ts-compat?tab=readme-ov-file frontend/package.json diff --git a/eslint.config.js b/eslint.config.js index 4eb2464..bd9a3ba 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,41 +1,45 @@ import globals from "globals"; import pluginJs from "@eslint/js"; -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import typescriptParser from "@typescript-eslint/parser"; +import tseslint from "typescript-eslint"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; export default [ - { - files: ["src/**/*.{js,mjs,cjs,ts,tsx}", "example/**/*.{js,mjs,cjs,ts,tsx}"], - }, { ignores: [ "dist/**", "eslint.config.js", - "**/_generated/", "vitest.config.ts", + "**/_generated/", + "node10stubs.mjs", ], }, { + files: ["src/**/*.{js,mjs,cjs,ts,tsx}", "example/**/*.{js,mjs,cjs,ts,tsx}"], languageOptions: { - globals: { - ...globals.worker, - ...globals.node, - }, - parser: typescriptParser, + parser: tseslint.parser, parserOptions: { - project: true, - tsconfigRootDir: ".", + project: [ + "./tsconfig.json", + "./example/tsconfig.json", + "./example/convex/tsconfig.json", + ], + tsconfigRootDir: import.meta.dirname, }, }, - plugins: { - "@typescript-eslint": typescriptEslint, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + // Convex code - Worker environment + { + files: ["src/**/*.{ts,tsx}", "example/convex/**/*.{ts,tsx}"], + ignores: ["src/react/**"], + languageOptions: { + globals: globals.worker, }, rules: { - ...typescriptEslint.configs["recommended"].rules, - ...pluginJs.configs.recommended.rules, "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-explicit-any": "off", - // allow (_arg: number) => {} and const _foo = 1; "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "warn", @@ -44,6 +48,52 @@ export default [ varsIgnorePattern: "^_", }, ], + "@typescript-eslint/no-unused-expressions": [ + "error", + { + allowShortCircuit: true, + allowTernary: true, + allowTaggedTemplates: true, + }, + ], + }, + }, + // React app code - Browser environment + { + files: ["src/react/**/*.{ts,tsx}", "example/src/**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-explicit-any": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, + }, + // Example config files (vite.config.ts, etc.) - Node environment + { + files: ["example/vite.config.ts", "example/**/*.config.{js,ts}"], + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, }, }, ]; diff --git a/example/convex/tsconfig.json b/example/convex/tsconfig.json index d3d31bf..68b4b18 100644 --- a/example/convex/tsconfig.json +++ b/example/convex/tsconfig.json @@ -1,24 +1,19 @@ { - /* This TypeScript project config describes the environment that - * Convex functions run in and is used to typecheck them. - * You can modify it, but some settings required to use Convex. - */ "compilerOptions": { - /* These settings are not required by Convex and can be modified. */ "allowJs": true, "strict": true, "skipLibCheck": true, - - /* These compiler options are required by Convex */ "target": "ESNext", - "lib": ["ES2021", "dom", "ESNext.Array"], + "lib": ["ES2021", "dom", "DOM.Iterable", "ESNext.Array"], "forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true, + "verbatimModuleSyntax": true, "module": "ESNext", "moduleResolution": "Bundler", "isolatedModules": true, - "noEmit": true + "noEmit": true, + "jsx": "react-jsx" }, - "include": ["./**/*"], + "include": [".*"], "exclude": ["./_generated"] } diff --git a/package-lock.json b/package-lock.json index 1f71a18..9724fab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.38.0", - "@types/node": "22.18.12", + "@types/node": "20.19.24", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", @@ -36,7 +36,7 @@ }, "peerDependencies": { "@convex-dev/workpool": "^0.2.19", - "convex": ">=1.25.0 <1.35.0", + "convex": "^1.24.8", "convex-helpers": "^0.1.99" } }, @@ -1438,9 +1438,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", - "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", "peer": true, @@ -2165,6 +2165,7 @@ "version": "1.28.0", "resolved": "https://registry.npmjs.org/convex/-/convex-1.28.0.tgz", "integrity": "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA==", + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -2201,6 +2202,7 @@ "integrity": "sha512-FISEUHjTKZFk4GE2jZZt6AvTFZCrzoSW6aXJUTY5IIlfuFvJCry3i6YMvOC3lL6ivR75lVEsGt56rwGP2kCcNQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "convex-helpers": "bin.cjs" }, diff --git a/package.json b/package.json index 90ea5e7..bbc4f2e 100644 --- a/package.json +++ b/package.json @@ -18,19 +18,19 @@ "dev:frontend": "cd example && vite --clearScreen false", "dev:build": "npx chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npx convex codegen --component ./src/component && npm run build' --initial", "predev": "npm run dev:backend -- --until-success", - "clean": "rm -rf dist tsconfig.build.tsbuildinfo", - "build": "tsc --project ./tsconfig.build.json && npm run copy:dts", - "copy:dts": "rsync -a --include='*/' --include='*.d.ts' --exclude='*' src/ dist/ || cpy 'src/**/*.d.ts' 'dist/' --parents", - "typecheck": "tsc --noEmit && tsc -p example/convex", - "lint": "eslint src && eslint example/convex", - "all": "run-p -r 'dev:backend' 'dev:frontend' 'build:watch' 'test:watch'", + "clean": "rm -rf dist *.tsbuildinfo", + "build": "tsc --project ./tsconfig.build.json", + "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex", + "lint": "eslint src && eslint example", + "all": "run-p -r 'dev:*' 'test:watch'", "test": "vitest run --typecheck", "test:watch": "vitest --typecheck --clearScreen false", "test:debug": "vitest --inspect-brk --no-file-parallelism", "test:coverage": "vitest run --coverage --coverage.reporter=text", + "attw": "attw $(npm pack -s) --exclude-entrypoints ./convex.config --profile esm-only", "prepare": "npm run build", - "alpha": "npm run clean && npm ci && run-p test lint typecheck && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", - "release": "npm run clean && npm ci && run-p test lint typecheck && npm version patch && npm publish && git push --tags", + "alpha": "npm run clean && npm ci && run-p test lint typecheck attw && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", + "release": "npm run clean && npm ci && run-p test lint typecheck attw && npm version patch && npm publish && git push --tags", "version": "pbcopy <<<$npm_package_version; vim CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md" }, "files": [ @@ -44,6 +44,9 @@ "default": "./dist/client/index.js" }, "./test": "./src/test.ts", + "./_generated/component.js": { + "types": "./dist/component/_generated/component.d.ts" + }, "./convex.config": { "types": "./dist/component/convex.config.d.ts", "default": "./dist/component/convex.config.js" @@ -51,7 +54,7 @@ }, "peerDependencies": { "@convex-dev/workpool": "^0.2.19", - "convex": ">=1.25.0 <1.35.0", + "convex": "^1.24.8", "convex-helpers": "^0.1.99" }, "dependencies": { @@ -62,7 +65,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.38.0", - "@types/node": "22.18.12", + "@types/node": "20.19.24", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", @@ -80,7 +83,6 @@ "typescript-eslint": "8.40.0", "vitest": "3.2.4" }, - "main": "./dist/client/index.js", "types": "./dist/client/index.d.ts", "module": "./dist/client/index.js" } diff --git a/src/test.ts b/src/test.ts index 7a340a7..4a0f38e 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,6 +1,5 @@ import type { TestConvex } from "convex-test"; import type { GenericSchema, SchemaDefinition } from "convex/server"; -import workpool from "@convex-dev/workpool/test"; import schema from "./component/schema.js"; const modules = import.meta.glob("./component/**/*.ts"); @@ -9,11 +8,10 @@ const modules = import.meta.glob("./component/**/*.ts"); * @param t - The test convex instance, e.g. from calling `convexTest`. * @param name - The name of the component, as registered in convex.config.ts. */ -function register( +export function register( t: TestConvex>, - name: string = "workflow", + name: string = "workflow" ) { t.registerComponent(name, schema, modules); - workpool.register(t, `${name}/workpool`); } export default { register, schema, modules }; From f2f755baee6b7a13900be799a2dcdf90f80727e4 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 4 Nov 2025 01:48:04 -0800 Subject: [PATCH 05/20] add test config --- tsconfig.test.json | 15 +++++++++++++++ vitest.config.ts | 3 +++ 2 files changed, 18 insertions(+) create mode 100644 tsconfig.test.json diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..51c78a1 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "example/src/**/*.ts", + "example/src/**/*.tsx", + "example/convex/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/_generated" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts index 28ce6fa..e9408f8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,5 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "edge-runtime", + typecheck: { + tsconfig: "./tsconfig.test.json", + }, }, }); From 4b36e3e7289df7759906aa8741e27bca386a2cde Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 4 Nov 2025 12:28:57 -0800 Subject: [PATCH 06/20] update configs --- eslint.config.js | 6 +- example/convex/setup.test.ts | 2 +- example/convex/tsconfig.json | 18 +- package-lock.json | 554 +++++++++++++++++++++++++++++++++ package.json | 6 +- src/client/environment.test.ts | 74 ++++- src/test.ts | 2 +- 7 files changed, 628 insertions(+), 34 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index bd9a3ba..636051d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,11 +19,7 @@ export default [ languageOptions: { parser: tseslint.parser, parserOptions: { - project: [ - "./tsconfig.json", - "./example/tsconfig.json", - "./example/convex/tsconfig.json", - ], + project: ["./tsconfig.json", "./example/convex/tsconfig.json"], tsconfigRootDir: import.meta.dirname, }, }, diff --git a/example/convex/setup.test.ts b/example/convex/setup.test.ts index 847f74d..bdd1af3 100644 --- a/example/convex/setup.test.ts +++ b/example/convex/setup.test.ts @@ -8,7 +8,7 @@ export const modules = import.meta.glob("./**/*.*s"); export function initConvexTest() { const t = convexTest(schema, modules); - t.registerComponent("workflow", workflow.schema, workflow.modules); + workflow.register(t); return t; } diff --git a/example/convex/tsconfig.json b/example/convex/tsconfig.json index 68b4b18..bab30c5 100644 --- a/example/convex/tsconfig.json +++ b/example/convex/tsconfig.json @@ -1,19 +1,17 @@ { "compilerOptions": { - "allowJs": true, - "strict": true, - "skipLibCheck": true, "target": "ESNext", - "lib": ["ES2021", "dom", "DOM.Iterable", "ESNext.Array"], - "forceConsistentCasingInFileNames": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "skipLibCheck": false, "allowSyntheticDefaultImports": true, - "verbatimModuleSyntax": true, + "strict": true, + "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Bundler", "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "noEmit": true }, - "include": [".*"], - "exclude": ["./_generated"] + "include": ["."], + "exclude": ["_generated"] } diff --git a/package-lock.json b/package-lock.json index 9724fab..41eb8af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.38.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", "globals": "16.4.0", "npm-run-all2": "8.0.4", "openai": "6.6.0", @@ -92,6 +94,274 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@convex-dev/workpool": { "version": "0.2.19", "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19.tgz", @@ -782,6 +1052,38 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.3.tgz", @@ -789,6 +1091,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jsdevtools/ez-spawn": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", @@ -1917,6 +2230,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", + "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -1959,6 +2282,41 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1995,6 +2353,27 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -2161,6 +2540,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/convex": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/convex/-/convex-1.28.0.tgz", @@ -2375,6 +2761,13 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -2430,6 +2823,16 @@ "@esbuild/win32-x64": "0.25.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2503,6 +2906,36 @@ } } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -2757,6 +3190,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2845,6 +3288,23 @@ "node": ">=8" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2970,6 +3430,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2999,6 +3472,19 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/junk": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", @@ -3077,6 +3563,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -3187,6 +3683,13 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4236,6 +4739,37 @@ "dev": true, "license": "ISC" }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4607,6 +5141,13 @@ "dev": true, "license": "ISC" }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", @@ -4738,6 +5279,19 @@ "engines": { "node": ">=20" } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index bbc4f2e..f371089 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ "dev": "run-p -r 'dev:*'", "dev:backend": "convex dev --typecheck-components", "dev:frontend": "cd example && vite --clearScreen false", - "dev:build": "npx chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npx convex codegen --component ./src/component && npm run build' --initial", + "dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'convex codegen --component-dir ./src/component && npm run build' --initial", "predev": "npm run dev:backend -- --until-success", "clean": "rm -rf dist *.tsbuildinfo", "build": "tsc --project ./tsconfig.build.json", "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex", - "lint": "eslint src && eslint example", + "lint": "eslint src", "all": "run-p -r 'dev:*' 'test:watch'", "test": "vitest run --typecheck", "test:watch": "vitest --typecheck --clearScreen false", @@ -74,6 +74,8 @@ "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.38.0", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", "globals": "16.4.0", "npm-run-all2": "8.0.4", "openai": "6.6.0", diff --git a/src/client/environment.test.ts b/src/client/environment.test.ts index 4644eb0..d3c9ae1 100644 --- a/src/client/environment.test.ts +++ b/src/client/environment.test.ts @@ -138,14 +138,23 @@ describe("environment patching units", () => { describe("behavior validation vs original Date", () => { it("should produce identical outputs for specific dates", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); // Test with specific timestamps const timestamps = [ 0, // Unix epoch 946684800000, // Y2K 1640995200000, // 2022-01-01 - 2023, 0, 15, 10, 30, 45, 123 // Feb 15, 2023 10:30:45.123 + 2023, + 0, + 15, + 10, + 30, + 45, + 123, // Feb 15, 2023 10:30:45.123 ]; for (const ts of [timestamps[0], timestamps[1], timestamps[2]]) { @@ -159,12 +168,22 @@ describe("environment patching units", () => { expect(deterministic.getHours()).toBe(original.getHours()); expect(deterministic.getMinutes()).toBe(original.getMinutes()); expect(deterministic.getSeconds()).toBe(original.getSeconds()); - expect(deterministic.getMilliseconds()).toBe(original.getMilliseconds()); + expect(deterministic.getMilliseconds()).toBe( + original.getMilliseconds(), + ); } // Test with constructor args const original = new Date(2023, 0, 15, 10, 30, 45, 123); - const deterministic = new DeterministicDate(2023, 0, 15, 10, 30, 45, 123); + const deterministic = new DeterministicDate( + 2023, + 0, + 15, + 10, + 30, + 45, + 123, + ); expect(deterministic.getFullYear()).toBe(original.getFullYear()); expect(deterministic.getMonth()).toBe(original.getMonth()); @@ -172,11 +191,16 @@ describe("environment patching units", () => { expect(deterministic.getHours()).toBe(original.getHours()); expect(deterministic.getMinutes()).toBe(original.getMinutes()); expect(deterministic.getSeconds()).toBe(original.getSeconds()); - expect(deterministic.getMilliseconds()).toBe(original.getMilliseconds()); + expect(deterministic.getMilliseconds()).toBe( + original.getMilliseconds(), + ); }); it("should produce identical string representations for deterministic dates", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); const timestamp = 1640995200000; // 2022-01-01T00:00:00.000Z const original = new Date(timestamp); @@ -192,7 +216,10 @@ describe("environment patching units", () => { }); it("should handle UTC methods identically", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); const timestamp = 1640995200123; // 2022-01-01T00:00:00.123Z const original = new Date(timestamp); @@ -204,12 +231,17 @@ describe("environment patching units", () => { expect(deterministic.getUTCHours()).toBe(original.getUTCHours()); expect(deterministic.getUTCMinutes()).toBe(original.getUTCMinutes()); expect(deterministic.getUTCSeconds()).toBe(original.getUTCSeconds()); - expect(deterministic.getUTCMilliseconds()).toBe(original.getUTCMilliseconds()); + expect(deterministic.getUTCMilliseconds()).toBe( + original.getUTCMilliseconds(), + ); expect(deterministic.getUTCDay()).toBe(original.getUTCDay()); }); it("should handle static methods identically", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); const dateString = "2023-01-15T10:30:45.123Z"; const year = 2023; @@ -220,13 +252,19 @@ describe("environment patching units", () => { const second = 45; const ms = 123; - expect(DeterministicDate.parse(dateString)).toBe(Date.parse(dateString)); - expect(DeterministicDate.UTC(year, month, day, hour, minute, second, ms)) - .toBe(Date.UTC(year, month, day, hour, minute, second, ms)); + expect(DeterministicDate.parse(dateString)).toBe( + Date.parse(dateString), + ); + expect( + DeterministicDate.UTC(year, month, day, hour, minute, second, ms), + ).toBe(Date.UTC(year, month, day, hour, minute, second, ms)); }); it("should maintain Date compatibility", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); const date = new DeterministicDate(2023, 0, 1); @@ -242,7 +280,10 @@ describe("environment patching units", () => { }); it("should handle Date modification methods correctly", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); const timestamp = 1640995200000; // 2022-01-01 const original = new Date(timestamp); @@ -264,7 +305,10 @@ describe("environment patching units", () => { }); it("should have timezone and locale methods available for future patching", () => { - const DeterministicDate = createDeterministicDate(Date, mockGetGenerationState); + const DeterministicDate = createDeterministicDate( + Date, + mockGetGenerationState, + ); const date = new DeterministicDate(1640995200000); // 2022-01-01T00:00:00.000Z // These methods exist but are not yet fully patched for determinism diff --git a/src/test.ts b/src/test.ts index 4a0f38e..4ae275c 100644 --- a/src/test.ts +++ b/src/test.ts @@ -10,7 +10,7 @@ const modules = import.meta.glob("./component/**/*.ts"); */ export function register( t: TestConvex>, - name: string = "workflow" + name: string = "workflow", ) { t.registerComponent(name, schema, modules); } From cccf0bdf77aa50b1e7cd0613f77e37f066f52fd5 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 4 Nov 2025 12:42:20 -0800 Subject: [PATCH 07/20] lint and format --- example/convex/example.ts | 6 ++-- example/convex/nestedWorkflow.ts | 2 +- example/convex/passingSignals.ts | 2 +- example/convex/transcription.ts | 8 ++--- example/convex/userConfirmation.ts | 4 +-- src/client/environment.test.ts | 54 +++++++++++++++--------------- src/client/environment.ts | 12 +++---- src/client/index.ts | 22 ++++++------ src/client/safeFunctionName.ts | 2 +- src/client/step.ts | 18 +++++----- src/client/validator.ts | 6 ++-- src/client/workflowContext.ts | 16 ++++----- src/client/workflowMutation.ts | 18 +++++----- src/component/event.ts | 22 ++++++------ src/component/journal.ts | 18 +++++----- src/component/logging.ts | 4 +-- src/component/model.ts | 4 +-- src/component/pool.ts | 16 ++++----- src/component/schema.ts | 8 ++--- src/component/setup.test.ts | 2 +- src/component/workflow.ts | 20 +++++------ src/test.ts | 2 +- src/types.ts | 6 ++-- 23 files changed, 136 insertions(+), 136 deletions(-) diff --git a/example/convex/example.ts b/example/convex/example.ts index 0230b0f..45d2d56 100644 --- a/example/convex/example.ts +++ b/example/convex/example.ts @@ -14,7 +14,7 @@ export const exampleWorkflow = workflow.define({ }, handler: async ( step, - args, + args // When returning things from other functions, you need to break the type // inference cycle by specifying the return type explicitly. ): Promise<{ @@ -45,7 +45,7 @@ export const exampleWorkflow = workflow.define({ const temp = Math.random() > 0.5 ? `${farenheit.toFixed(1)}°F` : `${temperature}°C`; console.log( - `Weather in ${name}: ${temp}, ${windSpeed} km/h, ${windGust} km/h`, + `Weather in ${name}: ${temp}, ${windSpeed} km/h, ${windGust} km/h` ); console.timeLog("weather", temperature); await step.runMutation(internal.example.updateFlow, { @@ -83,7 +83,7 @@ export const startWorkflow = internalMutation({ onComplete: internal.example.flowCompleted, context: { location }, startAsync: true, - }, + } ); await ctx.db.insert("flows", { workflowId: id, in: location, out: null }); return id; diff --git a/example/convex/nestedWorkflow.ts b/example/convex/nestedWorkflow.ts index ae4590f..31c8466 100644 --- a/example/convex/nestedWorkflow.ts +++ b/example/convex/nestedWorkflow.ts @@ -9,7 +9,7 @@ export const parentWorkflow = workflow.define({ console.log("Starting confirmation workflow"); const length = await ctx.runWorkflow( internal.nestedWorkflow.childWorkflow, - { foo: args.prompt }, + { foo: args.prompt } ); console.log("Length:", length); const stepResult = await ctx.runMutation(internal.nestedWorkflow.step, { diff --git a/example/convex/passingSignals.ts b/example/convex/passingSignals.ts index c6b947b..99478e0 100644 --- a/example/convex/passingSignals.ts +++ b/example/convex/passingSignals.ts @@ -12,7 +12,7 @@ export const signalBasedWorkflow = workflow.define({ for (let i = 0; i < 3; i++) { const signalId = await ctx.runMutation( internal.passingSignals.createSignal, - { workflowId: ctx.workflowId }, + { workflowId: ctx.workflowId } ); await ctx.awaitEvent({ id: signalId }); console.log("Signal received", signalId); diff --git a/example/convex/transcription.ts b/example/convex/transcription.ts index f4406c7..36fe74d 100644 --- a/example/convex/transcription.ts +++ b/example/convex/transcription.ts @@ -10,7 +10,7 @@ function getOpenAI() { if (!process.env.OPENAI_API_KEY) { throw new Error( "OPENAI_API_KEY is not configured.\n" + - "npx convex env set OPENAI_API_KEY sk-****", + "npx convex env set OPENAI_API_KEY sk-****" ); } return new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); @@ -29,7 +29,7 @@ export const startTranscription = internalMutation({ const id: string = await workflow.start( ctx, internal.transcription.transcriptionWorkflow, - { storageId: args.storageId }, + { storageId: args.storageId } ); return id; }, @@ -44,13 +44,13 @@ export const transcriptionWorkflow = workflow.define({ internal.transcription.computeTranscription, { storageId: args.storageId, - }, + } ); console.log(transcription); const embedding = await step.runAction( internal.transcription.computeEmbedding, { transcription }, - { retry: false }, + { retry: false } ); console.log(embedding.slice(0, 20)); }, diff --git a/example/convex/userConfirmation.ts b/example/convex/userConfirmation.ts index fd8892b..969cbb4 100644 --- a/example/convex/userConfirmation.ts +++ b/example/convex/userConfirmation.ts @@ -11,7 +11,7 @@ export const approvalEvent = defineEvent({ name: "approval" as const, validator: v.union( v.object({ approved: v.literal(true), choice: v.number() }), - v.object({ approved: v.literal(false), reason: v.string() }), + v.object({ approved: v.literal(false), reason: v.string() }) ), }); @@ -25,7 +25,7 @@ export const confirmationWorkflow = workflow.define({ const proposals = await ctx.runAction( internal.userConfirmation.generateProposals, { prompt: args.prompt }, - { retry: true }, + { retry: true } ); console.log("Proposals generated", proposals); const approval = await ctx.awaitEvent(approvalEvent); diff --git a/src/client/environment.test.ts b/src/client/environment.test.ts index d3c9ae1..e661c6b 100644 --- a/src/client/environment.test.ts +++ b/src/client/environment.test.ts @@ -70,7 +70,7 @@ describe("environment patching units", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); expect(DeterministicDate.now()).toBe(testTime); @@ -83,7 +83,7 @@ describe("environment patching units", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const date = new DeterministicDate(); @@ -93,7 +93,7 @@ describe("environment patching units", () => { it("should create Date with provided args", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const date = new DeterministicDate(2023, 0, 1); @@ -105,7 +105,7 @@ describe("environment patching units", () => { it("should return string when called without new", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const dateString = (DeterministicDate as unknown as () => string)(); @@ -116,7 +116,7 @@ describe("environment patching units", () => { const originalDate = Date; const DeterministicDate = createDeterministicDate( originalDate, - mockGetGenerationState, + mockGetGenerationState ); expect(DeterministicDate.parse).toBe(originalDate.parse); @@ -140,7 +140,7 @@ describe("environment patching units", () => { it("should produce identical outputs for specific dates", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); // Test with specific timestamps @@ -169,7 +169,7 @@ describe("environment patching units", () => { expect(deterministic.getMinutes()).toBe(original.getMinutes()); expect(deterministic.getSeconds()).toBe(original.getSeconds()); expect(deterministic.getMilliseconds()).toBe( - original.getMilliseconds(), + original.getMilliseconds() ); } @@ -182,7 +182,7 @@ describe("environment patching units", () => { 10, 30, 45, - 123, + 123 ); expect(deterministic.getFullYear()).toBe(original.getFullYear()); @@ -192,14 +192,14 @@ describe("environment patching units", () => { expect(deterministic.getMinutes()).toBe(original.getMinutes()); expect(deterministic.getSeconds()).toBe(original.getSeconds()); expect(deterministic.getMilliseconds()).toBe( - original.getMilliseconds(), + original.getMilliseconds() ); }); it("should produce identical string representations for deterministic dates", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const timestamp = 1640995200000; // 2022-01-01T00:00:00.000Z @@ -218,7 +218,7 @@ describe("environment patching units", () => { it("should handle UTC methods identically", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const timestamp = 1640995200123; // 2022-01-01T00:00:00.123Z @@ -232,7 +232,7 @@ describe("environment patching units", () => { expect(deterministic.getUTCMinutes()).toBe(original.getUTCMinutes()); expect(deterministic.getUTCSeconds()).toBe(original.getUTCSeconds()); expect(deterministic.getUTCMilliseconds()).toBe( - original.getUTCMilliseconds(), + original.getUTCMilliseconds() ); expect(deterministic.getUTCDay()).toBe(original.getUTCDay()); }); @@ -240,7 +240,7 @@ describe("environment patching units", () => { it("should handle static methods identically", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const dateString = "2023-01-15T10:30:45.123Z"; @@ -253,17 +253,17 @@ describe("environment patching units", () => { const ms = 123; expect(DeterministicDate.parse(dateString)).toBe( - Date.parse(dateString), + Date.parse(dateString) ); expect( - DeterministicDate.UTC(year, month, day, hour, minute, second, ms), + DeterministicDate.UTC(year, month, day, hour, minute, second, ms) ).toBe(Date.UTC(year, month, day, hour, minute, second, ms)); }); it("should maintain Date compatibility", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const date = new DeterministicDate(2023, 0, 1); @@ -282,7 +282,7 @@ describe("environment patching units", () => { it("should handle Date modification methods correctly", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const timestamp = 1640995200000; // 2022-01-01 @@ -307,7 +307,7 @@ describe("environment patching units", () => { it("should have timezone and locale methods available for future patching", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState, + mockGetGenerationState ); const date = new DeterministicDate(1640995200000); // 2022-01-01T00:00:00.000Z @@ -375,7 +375,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); proxiedConsole.log("test"); @@ -394,7 +394,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); // Methods should be functions (noop) but not call the original @@ -413,11 +413,11 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); expect(() => proxiedConsole.Console).toThrow( - "console.Console() is not supported within workflows", + "console.Console() is not supported within workflows" ); }); @@ -426,7 +426,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); proxiedConsole.count("test"); @@ -443,7 +443,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); proxiedConsole.count("test"); @@ -461,7 +461,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); proxiedConsole.groupEnd(); @@ -479,7 +479,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); proxiedConsole.time("test"); @@ -493,7 +493,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState, + mockGetGenerationState ); proxiedConsole.count("test"); diff --git a/src/client/environment.ts b/src/client/environment.ts index 80900d1..d1598a6 100644 --- a/src/client/environment.ts +++ b/src/client/environment.ts @@ -46,7 +46,7 @@ export function patchMath(math: typeof Math, seed: string): typeof Math { // Testable unit: creates deterministic Date constructor export function createDeterministicDate( originalDate: typeof Date, - getGenerationState: () => GenerationState, + getGenerationState: () => GenerationState ): typeof Date { function DeterministicDate(this: unknown, ...args: unknown[]) { // `Date()` was called directly, not as a constructor. @@ -59,7 +59,7 @@ export function createDeterministicDate( return new originalDate(now) as unknown as Date; } return new (originalDate as typeof Date)( - ...(args as ConstructorParameters), + ...(args as ConstructorParameters) ) as unknown as Date; } @@ -84,7 +84,7 @@ export function createDeterministicDate( export function setupEnvironment( getGenerationState: () => GenerationState, - workflowId: string, + workflowId: string ): void { const global = globalThis as Record; @@ -101,7 +101,7 @@ export function setupEnvironment( // Patch fetch global.fetch = (_input: RequestInfo | URL, _init?: RequestInit) => { throw new Error( - `Fetch isn't currently supported within workflows. Perform the fetch within an action and call it with step.runAction().`, + `Fetch isn't currently supported within workflows. Perform the fetch within an action and call it with step.runAction().` ); }; @@ -124,7 +124,7 @@ function noop() {} // exported for testing export function createConsole( console: Console, - getGenerationState: () => GenerationState, + getGenerationState: () => GenerationState ): Console { const counts: Record = {}; const times: Record = {}; @@ -152,7 +152,7 @@ export function createConsole( return target[prop]; case "Console": throw new Error( - "console.Console() is not supported within workflows", + "console.Console() is not supported within workflows" ); case "count": return (label?: string) => { diff --git a/src/client/index.ts b/src/client/index.ts index a0463d8..07601af 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -81,7 +81,7 @@ export type WorkflowDefinition< args?: ArgsValidator; handler: ( step: WorkflowCtx, - args: ObjectType, + args: ObjectType ) => Promise>; returns?: ReturnsValidator; workpoolOptions?: WorkpoolRetryOptions; @@ -98,7 +98,7 @@ export class WorkflowManager { public component: WorkflowComponent, public options?: { workpoolOptions: WorkpoolOptions; - }, + } ) {} /** @@ -111,7 +111,7 @@ export class WorkflowManager { ArgsValidator extends PropertyValidators, ReturnsValidator extends Validator | void, >( - workflow: WorkflowDefinition, + workflow: WorkflowDefinition ): RegisteredMutation< "internal", { @@ -125,7 +125,7 @@ export class WorkflowManager { return workflowMutation( this.component, workflow, - this.options?.workpoolOptions, + this.options?.workpoolOptions ); } @@ -156,7 +156,7 @@ export class WorkflowManager { startAsync?: boolean; /** @deprecated Use `startAsync` instead. */ validateAsync?: boolean; - }, + } ): Promise { const handle = await createFunctionHandle(workflow); const onComplete = options?.onComplete @@ -185,11 +185,11 @@ export class WorkflowManager { */ async status( ctx: RunQueryCtx, - workflowId: WorkflowId, + workflowId: WorkflowId ): Promise { const { workflow, inProgress } = await ctx.runQuery( this.component.workflow.getStatus, - { workflowId }, + { workflowId } ); const running = inProgress.map((entry) => entry.step as IdsToStrings); switch (workflow.runResult?.kind) { @@ -233,7 +233,7 @@ export class WorkflowManager { opts?: { order?: "asc" | "desc"; paginationOpts?: PaginationOptions; - }, + } ): Promise> { const steps = await ctx.runQuery(this.component.workflow.listSteps, { workflowId, @@ -277,9 +277,9 @@ export class WorkflowManager { | { validator?: undefined; value?: T } | { validator: Validator; value: T } | { error: string; value?: undefined } - ), + ) ): Promise> { - let result: RunResult = + const result: RunResult = "error" in args ? { kind: "failed", @@ -310,7 +310,7 @@ export class WorkflowManager { */ async createEvent( ctx: RunMutationCtx, - args: { name: Name; workflowId: WorkflowId }, + args: { name: Name; workflowId: WorkflowId } ): Promise> { return (await ctx.runMutation(this.component.event.create, { name: args.name, diff --git a/src/client/safeFunctionName.ts b/src/client/safeFunctionName.ts index 3497903..d852e6e 100644 --- a/src/client/safeFunctionName.ts +++ b/src/client/safeFunctionName.ts @@ -7,7 +7,7 @@ import { } from "convex/server"; export function safeFunctionName( - f: FunctionReference, + f: FunctionReference ) { const address = getFunctionAddress(f); return ( diff --git a/src/client/step.ts b/src/client/step.ts index 9fa2a7e..67a6732 100644 --- a/src/client/step.ts +++ b/src/client/step.ts @@ -61,11 +61,11 @@ export class StepExecutor { private journalEntries: Array, private receiver: BaseChannel, private now: number, - private workpoolOptions: WorkpoolOptions | undefined, + private workpoolOptions: WorkpoolOptions | undefined ) { this.journalEntrySize = journalEntries.reduce( (size, entry) => size + journalEntrySize(entry), - 0, + 0 ); if (this.journalEntrySize > MAX_JOURNAL_SIZE) { @@ -121,21 +121,21 @@ export class StepExecutor { completeMessage(message: StepRequest, entry: JournalEntry) { if (entry.step.inProgress) { throw new Error( - `Assertion failed: not blocked but have in-progress journal entry`, + `Assertion failed: not blocked but have in-progress journal entry` ); } const stepArgsJson = JSON.stringify(convexToJson(entry.step.args)); const messageArgsJson = JSON.stringify( - convexToJson(message.target.args as Value), + convexToJson(message.target.args as Value) ); if (stepArgsJson !== messageArgsJson) { throw new Error( - `Journal entry mismatch: ${entry.step.args} !== ${message.target.args}`, + `Journal entry mismatch: ${entry.step.args} !== ${message.target.args}` ); } if (entry.step.runResult === undefined) { throw new Error( - `Assertion failed: no outcome for completed function call`, + `Assertion failed: no outcome for completed function call` ); } switch (entry.step.runResult.kind) { @@ -189,7 +189,7 @@ export class StepExecutor { schedulerOptions: message.schedulerOptions, step, }; - }), + }) ); const entries = (await this.ctx.runMutation( this.component.journal.startSteps, @@ -198,14 +198,14 @@ export class StepExecutor { generationNumber: this.generationNumber, steps, workpoolOptions: this.workpoolOptions, - }, + } )) as JournalEntry[]; for (const entry of entries) { this.journalEntrySize += journalEntrySize(entry); if (this.journalEntrySize > MAX_JOURNAL_SIZE) { throw new Error( journalSizeError(this.journalEntrySize, this.workflowId) + - ` The failing step was ${entry.step.name} (${entry._id})`, + ` The failing step was ${entry.step.name} (${entry._id})` ); } } diff --git a/src/client/validator.ts b/src/client/validator.ts index a94b481..fb93bb9 100644 --- a/src/client/validator.ts +++ b/src/client/validator.ts @@ -7,7 +7,7 @@ import { export function checkArgs( args: Value, - validator: PropertyValidators | undefined, + validator: PropertyValidators | undefined ) { if (!validator) { return; @@ -20,7 +20,7 @@ export function checkArgs( function check( value: Value, - validator: GenericValidator, + validator: GenericValidator ): { ok: true } | { ok: false; message: string } { switch (validator.kind) { case "id": { @@ -149,7 +149,7 @@ function check( }; } for (const [field, fieldValue] of Object.entries( - value as Record, + value as Record )) { const keyResult = check(field, validator.key); if (!keyResult.ok) { diff --git a/src/client/workflowContext.ts b/src/client/workflowContext.ts index ea6a1aa..e0cb9fd 100644 --- a/src/client/workflowContext.ts +++ b/src/client/workflowContext.ts @@ -36,7 +36,7 @@ export type WorkflowCtx = { runQuery>( query: Query, args: FunctionArgs, - opts?: RunOptions, + opts?: RunOptions ): Promise>; /** @@ -49,7 +49,7 @@ export type WorkflowCtx = { runMutation>( mutation: Mutation, args: FunctionArgs, - opts?: RunOptions, + opts?: RunOptions ): Promise>; /** @@ -62,7 +62,7 @@ export type WorkflowCtx = { runAction>( action: Action, args: FunctionArgs, - opts?: RunOptions & RetryOption, + opts?: RunOptions & RetryOption ): Promise>; /** @@ -75,7 +75,7 @@ export type WorkflowCtx = { runWorkflow>( workflow: Workflow, args: FunctionArgs["args"], - opts?: RunOptions, + opts?: RunOptions ): Promise>; /** @@ -95,13 +95,13 @@ export type WorkflowCtx = { | { name?: Name; id: EventId } ) & { validator?: Validator; - }, + } ): Promise; }; export function createWorkflowCtx( workflowId: WorkflowId, - sender: BaseChannel, + sender: BaseChannel ) { return { workflowId, @@ -156,7 +156,7 @@ async function runFunction< functionType: FunctionType, f: F, args: unknown, - opts?: RunOptions & RetryOption, + opts?: RunOptions & RetryOption ): Promise { const { name, retry, ...schedulerOptions } = opts ?? {}; return run(sender, { @@ -174,7 +174,7 @@ async function runFunction< async function run( sender: BaseChannel, - request: Omit, + request: Omit ): Promise { let send: unknown; const p = new Promise((resolve, reject) => { diff --git a/src/client/workflowMutation.ts b/src/client/workflowMutation.ts index 5afb2a6..91e680f 100644 --- a/src/client/workflowMutation.ts +++ b/src/client/workflowMutation.ts @@ -34,7 +34,7 @@ const workflowArgs = v.union( v.object({ fn: v.string(), args: v.any(), - }), + }) ); const INVALID_WORKFLOW_MESSAGE = `Invalid arguments for workflow: Did you invoke the workflow with ctx.runMutation() instead of workflow.start()? Pro tip: to start a workflow directly from the CLI or dashboard, you can use args '{ fn: "path/to/file:workflowName", args: { ...your workflow args } }'`; @@ -45,7 +45,7 @@ const INVALID_WORKFLOW_MESSAGE = `Invalid arguments for workflow: Did you invoke export function workflowMutation( component: WorkflowComponent, registered: WorkflowDefinition, - defaultWorkpoolOptions?: WorkpoolOptions, + defaultWorkpoolOptions?: WorkpoolOptions ): RegisteredMutation< "internal", { @@ -76,7 +76,7 @@ export function workflowMutation( const { workflowId, generationNumber } = args; const { workflow, logLevel, journalEntries, ok } = await ctx.runQuery( component.journal.load, - { workflowId, shortCircuit: true }, + { workflowId, shortCircuit: true } ); const inProgress = journalEntries.filter(({ step }) => step.inProgress); const console = createLogger(logLevel); @@ -91,7 +91,7 @@ export function workflowMutation( } if (workflow.generationNumber !== generationNumber) { console.error( - `Invalid generation number: ${generationNumber} running workflow ${workflow.name} (${workflowId})`, + `Invalid generation number: ${generationNumber} running workflow ${workflow.name} (${workflowId})` ); return; } @@ -104,18 +104,18 @@ export function workflowMutation( `Workflow ${workflowId} blocked by ` + inProgress .map((entry) => `${entry.step.name} (${entry._id})`) - .join(", "), + .join(", ") ); return; } for (const journalEntry of journalEntries) { assert( !journalEntry.step.inProgress, - `Assertion failed: not blocked but have in-progress journal entry`, + `Assertion failed: not blocked but have in-progress journal entry` ); } const channel = new BaseChannel( - workpoolOptions.maxParallelism ?? 10, + workpoolOptions.maxParallelism ?? 10 ); const step = createWorkflowCtx(workflowId, channel); const executor = new StepExecutor( @@ -126,7 +126,7 @@ export function workflowMutation( journalEntries as JournalEntry[], channel, Date.now(), - workpoolOptions, + workpoolOptions ); setupEnvironment(executor.getGenerationState.bind(executor), workflowId); @@ -149,7 +149,7 @@ export function workflowMutation( : formatErrorWithStack(error); console.error( "Workflow handler returned invalid return value: ", - message, + message ); runResult = { kind: "failed", diff --git a/src/component/event.ts b/src/component/event.ts index a002316..b39f9ec 100644 --- a/src/component/event.ts +++ b/src/component/event.ts @@ -10,7 +10,7 @@ import { enqueueWorkflow, getWorkpool, workpoolOptions } from "./pool.js"; export async function awaitEvent( ctx: MutationCtx, entry: Doc<"steps">, - args: { eventId?: Id<"events">; name: string }, + args: { eventId?: Id<"events">; name: string } ) { const event = await getOrCreateEvent(ctx, entry.workflowId, args, [ "sent", @@ -19,12 +19,12 @@ export async function awaitEvent( switch (event.state.kind) { case "consumed": { throw new Error( - `Event already consumed: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})`, + `Event already consumed: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})` ); } case "waiting": { throw new Error( - `Event already waiting: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})`, + `Event already waiting: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})` ); } } @@ -67,13 +67,13 @@ async function getOrCreateEvent( ctx: MutationCtx, workflowId: Id<"workflows"> | undefined, args: { eventId?: Id<"events">; name?: string }, - statuses: Doc<"events">["state"]["kind"][], + statuses: Doc<"events">["state"]["kind"][] ): Promise> { if (args.eventId) { const event = await ctx.db.get(args.eventId); if (!event) { throw new Error( - `Event not found: ${args.eventId} (${args.name}) in workflow ${workflowId}`, + `Event not found: ${args.eventId} (${args.name}) in workflow ${workflowId}` ); } return event; @@ -84,7 +84,7 @@ async function getOrCreateEvent( const event = await ctx.db .query("events") .withIndex("workflowId_state", (q) => - q.eq("workflowId", workflowId).eq("state.kind", status), + q.eq("workflowId", workflowId).eq("state.kind", status) ) .filter((q) => q.eq(q.field("name"), args.name)) .first(); @@ -117,19 +117,19 @@ export const send = mutation({ eventId: args.eventId, name: args.name, }, - ["waiting", "created"], + ["waiting", "created"] ); const { workflowId } = event; const name = args.name ?? event.name; switch (event.state.kind) { case "sent": { throw new Error( - `Event already sent: ${event._id} (${name}) in workflow ${workflowId}`, + `Event already sent: ${event._id} (${name}) in workflow ${workflowId}` ); } case "consumed": { throw new Error( - `Event already consumed: ${event._id} (${name}) in workflow ${workflowId}`, + `Event already consumed: ${event._id} (${name}) in workflow ${workflowId}` ); } case "created": { @@ -142,7 +142,7 @@ export const send = mutation({ const step = await ctx.db.get(event.state.stepId); assert( step, - `Entry ${event.state.stepId} not found when sending event ${event._id} (${name}) in workflow ${workflowId}`, + `Entry ${event.state.stepId} not found when sending event ${event._id} (${name}) in workflow ${workflowId}` ); assert(step.step.kind === "event", "Step is not an event"); step.step.eventId = event._id; @@ -162,7 +162,7 @@ export const send = mutation({ const anyMoreEvents = await ctx.db .query("events") .withIndex("workflowId_state", (q) => - q.eq("workflowId", workflowId).eq("state.kind", "waiting"), + q.eq("workflowId", workflowId).eq("state.kind", "waiting") ) .order("desc") .first(); diff --git a/src/component/journal.ts b/src/component/journal.ts index 62aeafd..758050d 100644 --- a/src/component/journal.ts +++ b/src/component/journal.ts @@ -45,7 +45,7 @@ export const load = query({ const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => - q.eq("step.inProgress", true).eq("workflowId", workflowId), + q.eq("step.inProgress", true).eq("workflowId", workflowId) ) .first(); if (inProgress) { @@ -82,10 +82,10 @@ export const startSteps = mutation({ schedulerOptions: v.optional( v.union( v.object({ runAt: v.optional(v.number()) }), - v.object({ runAfter: v.optional(v.number()) }), - ), + v.object({ runAfter: v.optional(v.number()) }) + ) ), - }), + }) ), workpoolOptions: v.optional(workpoolOptions), }, @@ -147,7 +147,7 @@ export const startSteps = mutation({ maxParallelism: args.workpoolOptions?.maxParallelism, onComplete: { fnHandle: await createFunctionHandle( - internal.pool.nestedWorkflowOnComplete, + internal.pool.nestedWorkflowOnComplete ), context: { stepId, @@ -171,7 +171,7 @@ export const startSteps = mutation({ ctx, step.handle as FunctionHandle<"query">, step.args, - { context, onComplete, name, ...schedulerOptions }, + { context, onComplete, name, ...schedulerOptions } ); break; } @@ -180,7 +180,7 @@ export const startSteps = mutation({ ctx, step.handle as FunctionHandle<"mutation">, step.args, - { context, onComplete, name, ...schedulerOptions }, + { context, onComplete, name, ...schedulerOptions } ); break; } @@ -189,7 +189,7 @@ export const startSteps = mutation({ ctx, step.handle as FunctionHandle<"action">, step.args, - { context, onComplete, name, retry, ...schedulerOptions }, + { context, onComplete, name, retry, ...schedulerOptions } ); break; } @@ -205,7 +205,7 @@ export const startSteps = mutation({ stepNumber, }); return entry; - }), + }) ); return entries; }, diff --git a/src/component/logging.ts b/src/component/logging.ts index 18ce9e4..8ca55e8 100644 --- a/src/component/logging.ts +++ b/src/component/logging.ts @@ -10,7 +10,7 @@ export const logLevel = v.union( v.literal("INFO"), v.literal("REPORT"), v.literal("WARN"), - v.literal("ERROR"), + v.literal("ERROR") ); export type LogLevel = Infer; @@ -32,7 +32,7 @@ const logLevelByName = logLevelOrder.reduce( acc[l] = i; return acc; }, - {} as Record, + {} as Record ); export function shouldLog(config: LogLevel, level: LogLevel) { return logLevelByName[config] <= logLevelByName[level]; diff --git a/src/component/model.ts b/src/component/model.ts index 8a5f46e..ec6c3a1 100644 --- a/src/component/model.ts +++ b/src/component/model.ts @@ -3,7 +3,7 @@ import type { QueryCtx } from "./_generated/server.js"; export async function getWorkflow( ctx: QueryCtx, workflowIdStr: string, - expectedGenerationNumber: number | null, + expectedGenerationNumber: number | null ) { const workflowId = ctx.db.normalizeId("workflows", workflowIdStr); if (!workflowId) { @@ -18,7 +18,7 @@ export async function getWorkflow( workflow.generationNumber !== expectedGenerationNumber ) { throw new Error( - `Invalid generation number: ${expectedGenerationNumber} for workflow ${workflow.name} (${workflowId})`, + `Invalid generation number: ${expectedGenerationNumber} for workflow ${workflow.name} (${workflowId})` ); } return workflow; diff --git a/src/component/pool.ts b/src/component/pool.ts index ef63afb..b72e32e 100644 --- a/src/component/pool.ts +++ b/src/component/pool.ts @@ -42,7 +42,7 @@ export const DEFAULT_RETRY_BEHAVIOR = { export async function getWorkpool( ctx: MutationCtx, - opts: WorkpoolOptions | undefined, + opts: WorkpoolOptions | undefined ) { // nit: can fetch config only if necessary const config = await ctx.db.query("config").first(); @@ -94,7 +94,7 @@ async function onCompleteHandler( workflowId?: WorkflowId; result: RunResult; context: object; - }, + } ) { const console = await getDefaultLogger(ctx); const stepId = @@ -130,13 +130,13 @@ async function onCompleteHandler( const workflow = await getWorkflow(ctx, workflowId, null); if (workflow.generationNumber !== generationNumber) { console.error( - `Workflow: ${workflowId} already has generation number ${workflow.generationNumber} when completing ${stepId}`, + `Workflow: ${workflowId} already has generation number ${workflow.generationNumber} when completing ${stepId}` ); return; } if (!journalEntry.step.inProgress) { console.error( - `Step finished but journal entry not in progress: ${stepId} status: ${journalEntry.step.runResult?.kind ?? "pending"}`, + `Step finished but journal entry not in progress: ${stepId} status: ${journalEntry.step.runResult?.kind ?? "pending"}` ); return; } @@ -175,7 +175,7 @@ async function onCompleteHandler( if (workflow.runResult !== undefined) { if (workflow.runResult.kind !== "canceled") { console.error( - `Workflow: ${workflowId} already ${workflow.runResult.kind} when completing ${stepId} with status ${args.result.kind}`, + `Workflow: ${workflowId} already ${workflow.runResult.kind} when completing ${stepId} with status ${args.result.kind}` ); } return; @@ -187,7 +187,7 @@ async function onCompleteHandler( export async function enqueueWorkflow( ctx: MutationCtx, workflow: Doc<"workflows">, - workpool: Workpool, + workpool: Workpool ) { const { _id: workflowId, generationNumber, name, workflowHandle } = workflow; await workpool.enqueueMutation( @@ -198,7 +198,7 @@ export async function enqueueWorkflow( name, onComplete: internal.pool.handlerOnComplete, context: { workflowId, generationNumber }, - }, + } ); } @@ -233,7 +233,7 @@ export const handlerOnComplete = internalMutation({ console.error("Invalid handlerOnComplete context", args.context); const workflowId = ctx.db.normalizeId( "workflows", - args.context.workflowId, + args.context.workflowId ); await ctx.db.insert("onCompleteFailures", args); if (!workflowId) { diff --git a/src/component/schema.ts b/src/component/schema.ts index 0cfdd3c..3a12544 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -88,7 +88,7 @@ export const step = v.union( ...stepCommonFields, eventId: v.optional(v.id("events")), args: v.object({ eventId: v.optional(v.id("events")) }), - }), + }) ); export type Step = Infer; @@ -167,7 +167,7 @@ export const event = { sentAt: v.number(), consumedAt: v.number(), stepId: v.id("steps"), - }), + }) ), }; @@ -197,7 +197,7 @@ export default defineSchema({ generationNumber: v.number(), runResult: vResultValidator, error: v.string(), - }), - ), + }) + ) ), }); diff --git a/src/component/setup.test.ts b/src/component/setup.test.ts index 8cbd7dd..b47f2e1 100644 --- a/src/component/setup.test.ts +++ b/src/component/setup.test.ts @@ -8,7 +8,7 @@ export const modules = import.meta.glob("./**/*.*s"); import componentSchema from "../../node_modules/@convex-dev/workpool/src/component/schema.js"; export { componentSchema }; export const componentModules = import.meta.glob( - "../../node_modules/@convex-dev/workpool/src/component/**/*.ts", + "../../node_modules/@convex-dev/workpool/src/component/**/*.ts" ); export function initConvexTest() { const t = convexTest(schema, modules); diff --git a/src/component/workflow.ts b/src/component/workflow.ts index 3f896e0..b242b6a 100644 --- a/src/component/workflow.ts +++ b/src/component/workflow.ts @@ -49,7 +49,7 @@ export const create = mutation({ export async function createHandler( ctx: MutationCtx, args: Infer, - schedulerOptions?: SchedulerOptions, + schedulerOptions?: SchedulerOptions ) { const console = await getDefaultLogger(ctx); await updateMaxParallelism(ctx, console, args.maxParallelism); @@ -63,7 +63,7 @@ export async function createHandler( console.debug( `Created workflow ${workflowId}:`, args.workflowArgs, - args.workflowHandle, + args.workflowHandle ); if (args.startAsync) { const workpool = await getWorkpool(ctx, args); @@ -76,7 +76,7 @@ export async function createHandler( onComplete: internal.pool.handlerOnComplete, context: { workflowId, generationNumber: 0 }, ...schedulerOptions, - }, + } ); } else { // If we can't start it, may as well not create it, eh? Fail fast... @@ -105,7 +105,7 @@ export const getStatus = query({ const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => - q.eq("step.inProgress", true).eq("workflowId", args.workflowId), + q.eq("step.inProgress", true).eq("workflowId", args.workflowId) ) .collect(); console.debug(`${args.workflowId} blocked by`, inProgress); @@ -198,12 +198,12 @@ export const complete = mutation({ // When the overall workflow completes (successfully or not). export async function completeHandler( ctx: MutationCtx, - args: Infer, + args: Infer ) { const workflow = await getWorkflow( ctx, args.workflowId, - args.generationNumber, + args.generationNumber ); const console = await getDefaultLogger(ctx); if (workflow.runResult) { @@ -223,7 +223,7 @@ export async function completeHandler( const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => - q.eq("step.inProgress", true).eq("workflowId", args.workflowId), + q.eq("step.inProgress", true).eq("workflowId", args.workflowId) ) .collect(); if (inProgress.length > 0) { @@ -257,7 +257,7 @@ export async function completeHandler( workflowId: workflow._id as unknown as WorkflowId, result: workflow.runResult, context: workflow.onComplete.context, - }, + } ); } catch (error) { const message = formatErrorWithStack(error); @@ -290,7 +290,7 @@ export const cleanup = mutation({ // TODO: allow cleaning up a workflow from inside it / in the onComplete hook if (!workflow.runResult) { logger.debug( - `Can't clean up workflow ${workflowId} since it hasn't completed.`, + `Can't clean up workflow ${workflowId} since it hasn't completed.` ); return false; } @@ -311,7 +311,7 @@ export const cleanup = mutation({ async function updateMaxParallelism( ctx: MutationCtx, console: Logger, - maxParallelism: number | undefined, + maxParallelism: number | undefined ) { const config = await ctx.db.query("config").first(); if (config) { diff --git a/src/test.ts b/src/test.ts index 4ae275c..4a0f38e 100644 --- a/src/test.ts +++ b/src/test.ts @@ -10,7 +10,7 @@ const modules = import.meta.glob("./component/**/*.ts"); */ export function register( t: TestConvex>, - name: string = "workflow", + name: string = "workflow" ) { t.registerComponent(name, schema, modules); } diff --git a/src/types.ts b/src/types.ts index f7a7d6d..59b4a84 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,7 @@ export const vWorkflowStep = v.object({ kind: v.union( v.literal("function"), v.literal("workflow"), - v.literal("event"), + v.literal("event") ), workId: v.optional(vWorkIdValidator), nestedWorkflowId: v.optional(vWorkflowId), @@ -110,8 +110,8 @@ export function vPaginationResult< v.union( v.literal("SplitRecommended"), v.literal("SplitRequired"), - v.null(), - ), + v.null() + ) ), }); } From 54865a4e1b6a406092b6ad8b9fef1453d6ccf59c Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 4 Nov 2025 14:09:50 -0800 Subject: [PATCH 08/20] format --- .prettierrc.json | 4 ++ CHANGELOG.md | 20 ++++--- README.md | 94 ++++++++++++++++-------------- example/convex/example.ts | 6 +- example/convex/nestedWorkflow.ts | 2 +- example/convex/passingSignals.ts | 2 +- example/convex/transcription.ts | 8 +-- example/convex/userConfirmation.ts | 4 +- package.json | 2 +- src/client/environment.test.ts | 54 ++++++++--------- src/client/environment.ts | 12 ++-- src/client/index.ts | 20 +++---- src/client/safeFunctionName.ts | 2 +- src/client/step.ts | 18 +++--- src/client/validator.ts | 6 +- src/client/workflowContext.ts | 16 ++--- src/client/workflowMutation.ts | 18 +++--- src/component/event.ts | 22 +++---- src/component/journal.ts | 18 +++--- src/component/logging.ts | 4 +- src/component/model.ts | 4 +- src/component/pool.ts | 16 ++--- src/component/schema.ts | 8 +-- src/component/setup.test.ts | 2 +- src/component/workflow.ts | 20 +++---- src/test.ts | 2 +- src/types.ts | 6 +- tsconfig.test.json | 6 +- 28 files changed, 202 insertions(+), 194 deletions(-) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f3877f7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "proseWrap": "always" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f862fd..59cf742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,18 @@ ## 0.2.8 alpha -- Adds asynchronous events - wait for an event in a workflow, send - events asynchronously - allows pause/resume, human-in-loop, etc. +- Adds asynchronous events - wait for an event in a workflow, send events + asynchronously - allows pause/resume, human-in-loop, etc. - Supports nested workflows with step.runWorkflow. - Surfaces return value of the workflow in the status - You can start a workflow directly from the CLI / dashboard without having to make a mutation to call workflow.start: - `{ fn: "path/to/file:workflowName", args: { ...your workflow args } }` -- Reduces read bandwidth when reading the journal after running many steps in parallel. -- Simplifies the onComplete type requirement so you can accept a workflowId as a string. - This helps when you have statically generated types which can't do branded strings. +- Reduces read bandwidth when reading the journal after running many steps in + parallel. +- Simplifies the onComplete type requirement so you can accept a workflowId as a + string. This helps when you have statically generated types which can't do + branded strings. - Adds a /test entrypoint to make testing easier - Exports the `WorkflowCtx` and `WorkflowStep` types - Support for Math.random via seeded PRNG. @@ -23,8 +25,8 @@ - Batches the call to start steps - Adds the workflow name to the workpool execution for observability - Logs any error that shows up in the workflow body -- Will call onComplete for Workflows with startAsync that fail - on their first invocation. +- Will call onComplete for Workflows with startAsync that fail on their first + invocation. - Increases the max journal size from 1MB to 8MB - Adds the WorkflowId type to step.workflowId - Exposes /test entrypoint to make testing easier @@ -39,5 +41,5 @@ - Call the onComplete handler for canceled workflows - Canceling is more graceful - canceled steps generally won't print errors -- Allow `startAsync` to enqueue the starting of the workflow - to allow starting many workflows safely. +- Allow `startAsync` to enqueue the starting of the workflow to allow starting + many workflows safely. diff --git a/README.md b/README.md index 0402c02..55fb201 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,12 @@ export const userOnboarding = workflow.define({ }); ``` -This component adds durably executed _workflows_ to Convex. Combine Convex queries, mutations, -and actions into long-lived workflows, and the system will always fully execute a workflow -to completion. +This component adds durably executed _workflows_ to Convex. Combine Convex +queries, mutations, and actions into long-lived workflows, and the system will +always fully execute a workflow to completion. -Open a [GitHub issue](https://github.com/get-convex/workflow/issues) with any feedback or bugs you find. +Open a [GitHub issue](https://github.com/get-convex/workflow/issues) with any +feedback or bugs you find. ## Installation @@ -97,8 +98,8 @@ app.use(workflow); export default app; ``` -Finally, create a workflow manager within your `convex/` folder, and point it -to the installed component: +Finally, create a workflow manager within your `convex/` folder, and point it to +the installed component: ```ts // convex/index.ts @@ -114,13 +115,13 @@ The first step is to define a workflow using `workflow.define()`. This function is designed to feel like a Convex action but with a few restrictions: 1. The workflow runs in the background, so it can't return a value. -2. The workflow must be _deterministic_, so it should implement most of its logic - by calling out to other Convex functions. We restrict access to some +2. The workflow must be _deterministic_, so it should implement most of its + logic by calling out to other Convex functions. We restrict access to some non-deterministic functions like `fetch` and `crypto`. Others we patch, such as `console` for logging, `Math.random()` (seeded PRNG) and `Date` for time. -Note: To help avoid type cycles, always annotate the return type of the `handler` -with the return type of the workflow. +Note: To help avoid type cycles, always annotate the return type of the +`handler` with the return type of the workflow. ```ts export const exampleWorkflow = workflow.define({ @@ -157,8 +158,8 @@ export const exampleAction = internalAction({ ### Starting a workflow -Once you've defined a workflow, you can start it from a mutation or action -using `workflow.start()`. +Once you've defined a workflow, you can start it from a mutation or action using +`workflow.start()`. ```ts export const kickoffWorkflow = mutation({ @@ -228,8 +229,8 @@ export const handleOnComplete = mutation({ ### Running steps in parallel -You can run steps in parallel by calling `step.runAction()` multiple times in -a `Promise.all()` call. +You can run steps in parallel by calling `step.runAction()` multiple times in a +`Promise.all()` call. ```ts export const exampleWorkflow = workflow.define({ @@ -243,20 +244,21 @@ export const exampleWorkflow = workflow.define({ }); ``` -Note: The workflow will not proceed until all steps fired off at once have completed. +Note: The workflow will not proceed until all steps fired off at once have +completed. ### Specifying retry behavior Sometimes actions fail due to transient errors, whether it was an unreliable third-party API or a server restart. You can have the workflow automatically -retry actions using best practices (exponential backoff & jitter). -By default there are no retries, and the workflow will fail. +retry actions using best practices (exponential backoff & jitter). By default +there are no retries, and the workflow will fail. You can specify default retry behavior for all workflows on the WorkflowManager, or override it on a per-workflow basis. -You can also specify a custom retry behavior per-step, to opt-out of retries -for actions that may want at-most-once semantics. +You can also specify a custom retry behavior per-step, to opt-out of retries for +actions that may want at-most-once semantics. Workpool options: @@ -270,8 +272,8 @@ If you specify any of these, it will override the - `retryActionsByDefault`: Whether to retry actions, by default is false. - If you specify a retry behavior at the step level, it will always retry. -At the step level, you can also specify `true` or `false` to disable or use -the default policy. +At the step level, you can also specify `true` or `false` to disable or use the +default policy. ```ts const workflow = new WorkflowManager(components.workflow, { @@ -306,11 +308,10 @@ export const exampleWorkflow = workflow.define({ ### Specifying step parallelism You can specify how many steps can run in parallel by setting the -`maxParallelism` workpool option. It has a reasonable default. -On the free tier, you should not exceed 20, otherwise your other scheduled -functions may become delayed while competing for available functions with your -workflow steps. -On a Pro account, you should not exceed 100 across all your workflows and workpools. +`maxParallelism` workpool option. It has a reasonable default. On the free tier, +you should not exceed 20, otherwise your other scheduled functions may become +delayed while competing for available functions with your workflow steps. On a +Pro account, you should not exceed 100 across all your workflows and workpools. If you want to do a lot of work in parallel, you should employ batching, where each workflow operates on a batch of work, e.g. scraping a list of links instead of one link per workflow. @@ -328,8 +329,8 @@ const workflow = new WorkflowManager(components.workflow, { ### Checking a workflow's status -The `workflow.start()` method returns a `WorkflowId`, which can then be used for querying -a workflow's status. +The `workflow.start()` method returns a `WorkflowId`, which can then be used for +querying a workflow's status. ```ts export const kickoffWorkflow = action({ @@ -349,8 +350,9 @@ export const kickoffWorkflow = action({ ### Canceling a workflow -You can cancel a workflow with `workflow.cancel()`, halting the workflow's execution immmediately. -In-progress calls to `step.runAction()`, however, will finish executing. +You can cancel a workflow with `workflow.cancel()`, halting the workflow's +execution immmediately. In-progress calls to `step.runAction()`, however, will +finish executing. ```ts export const kickoffWorkflow = action({ @@ -370,8 +372,9 @@ export const kickoffWorkflow = action({ ### Cleaning up a workflow -After a workflow has completed, you can clean up its storage with `workflow.cleanup()`. -Completed workflows are not automatically cleaned up by the system. +After a workflow has completed, you can clean up its storage with +`workflow.cleanup()`. Completed workflows are not automatically cleaned up by +the system. ```ts export const kickoffWorkflow = action({ @@ -402,8 +405,8 @@ export const kickoffWorkflow = action({ You can specify a custom name for a step by passing a `name` option to the step. -This allows the events emitted to your logs to be more descriptive. -By default it uses the `file/folder:function` name. +This allows the events emitted to your logs to be more descriptive. By default +it uses the `file/folder:function` name. ```ts export const exampleWorkflow = workflow.define({ @@ -418,9 +421,11 @@ export const exampleWorkflow = workflow.define({ ### Circular dependencies -Having the return value of workflows depend on other Convex functions can lead to circular dependencies due to the -`internal.foo.bar` way of specifying functions. The way to fix this is to explicitly type the return value of the -workflow. When in doubt, add return types to more `handler` functions, like this: +Having the return value of workflows depend on other Convex functions can lead +to circular dependencies due to the `internal.foo.bar` way of specifying +functions. The way to fix this is to explicitly type the return value of the +workflow. When in doubt, add return types to more `handler` functions, like +this: ```diff export const supportAgentWorkflow = workflow.define({ @@ -441,8 +446,8 @@ workflow. When in doubt, add return types to more `handler` functions, like this ### More concise workflows -To avoid the noise of `internal.foo.*` syntax, you can use a variable. -For instance, if you define all your steps in `convex/steps.ts`, you can do this: +To avoid the noise of `internal.foo.*` syntax, you can use a variable. For +instance, if you define all your steps in `convex/steps.ts`, you can do this: ```diff const s = internal.steps; @@ -469,15 +474,16 @@ Here are a few limitations to keep in mind: mutation apply and limit the number and size of steps you can perform to 16MiB (including the workflow state overhead). See more about mutation limits here: https://docs.convex.dev/production/state/limits#transactions -- We currently do not collect backtraces from within function calls from workflows. +- We currently do not collect backtraces from within function calls from + workflows. - If you need to use side effects like `fetch` or use cryptographic randomness, you'll need to do that in a step, not in the workflow definition. - `Math.random` is deterministic and not suitable for cryptographic use. It is, however, useful for sharding, jitter, and other pseudo-random applications. - If the implementation of the workflow meaningfully changes (steps added, - removed, or reordered) then it will fail with a determinism violation. - The implementation should stay stable for the lifetime of active workflows. - See [this issue](https://github.com/get-convex/workflow/issues/35) for ideas - on how to make this better. + removed, or reordered) then it will fail with a determinism violation. The + implementation should stay stable for the lifetime of active workflows. See + [this issue](https://github.com/get-convex/workflow/issues/35) for ideas on + how to make this better. diff --git a/example/convex/example.ts b/example/convex/example.ts index 45d2d56..0230b0f 100644 --- a/example/convex/example.ts +++ b/example/convex/example.ts @@ -14,7 +14,7 @@ export const exampleWorkflow = workflow.define({ }, handler: async ( step, - args + args, // When returning things from other functions, you need to break the type // inference cycle by specifying the return type explicitly. ): Promise<{ @@ -45,7 +45,7 @@ export const exampleWorkflow = workflow.define({ const temp = Math.random() > 0.5 ? `${farenheit.toFixed(1)}°F` : `${temperature}°C`; console.log( - `Weather in ${name}: ${temp}, ${windSpeed} km/h, ${windGust} km/h` + `Weather in ${name}: ${temp}, ${windSpeed} km/h, ${windGust} km/h`, ); console.timeLog("weather", temperature); await step.runMutation(internal.example.updateFlow, { @@ -83,7 +83,7 @@ export const startWorkflow = internalMutation({ onComplete: internal.example.flowCompleted, context: { location }, startAsync: true, - } + }, ); await ctx.db.insert("flows", { workflowId: id, in: location, out: null }); return id; diff --git a/example/convex/nestedWorkflow.ts b/example/convex/nestedWorkflow.ts index 31c8466..ae4590f 100644 --- a/example/convex/nestedWorkflow.ts +++ b/example/convex/nestedWorkflow.ts @@ -9,7 +9,7 @@ export const parentWorkflow = workflow.define({ console.log("Starting confirmation workflow"); const length = await ctx.runWorkflow( internal.nestedWorkflow.childWorkflow, - { foo: args.prompt } + { foo: args.prompt }, ); console.log("Length:", length); const stepResult = await ctx.runMutation(internal.nestedWorkflow.step, { diff --git a/example/convex/passingSignals.ts b/example/convex/passingSignals.ts index 99478e0..c6b947b 100644 --- a/example/convex/passingSignals.ts +++ b/example/convex/passingSignals.ts @@ -12,7 +12,7 @@ export const signalBasedWorkflow = workflow.define({ for (let i = 0; i < 3; i++) { const signalId = await ctx.runMutation( internal.passingSignals.createSignal, - { workflowId: ctx.workflowId } + { workflowId: ctx.workflowId }, ); await ctx.awaitEvent({ id: signalId }); console.log("Signal received", signalId); diff --git a/example/convex/transcription.ts b/example/convex/transcription.ts index 36fe74d..f4406c7 100644 --- a/example/convex/transcription.ts +++ b/example/convex/transcription.ts @@ -10,7 +10,7 @@ function getOpenAI() { if (!process.env.OPENAI_API_KEY) { throw new Error( "OPENAI_API_KEY is not configured.\n" + - "npx convex env set OPENAI_API_KEY sk-****" + "npx convex env set OPENAI_API_KEY sk-****", ); } return new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); @@ -29,7 +29,7 @@ export const startTranscription = internalMutation({ const id: string = await workflow.start( ctx, internal.transcription.transcriptionWorkflow, - { storageId: args.storageId } + { storageId: args.storageId }, ); return id; }, @@ -44,13 +44,13 @@ export const transcriptionWorkflow = workflow.define({ internal.transcription.computeTranscription, { storageId: args.storageId, - } + }, ); console.log(transcription); const embedding = await step.runAction( internal.transcription.computeEmbedding, { transcription }, - { retry: false } + { retry: false }, ); console.log(embedding.slice(0, 20)); }, diff --git a/example/convex/userConfirmation.ts b/example/convex/userConfirmation.ts index 969cbb4..fd8892b 100644 --- a/example/convex/userConfirmation.ts +++ b/example/convex/userConfirmation.ts @@ -11,7 +11,7 @@ export const approvalEvent = defineEvent({ name: "approval" as const, validator: v.union( v.object({ approved: v.literal(true), choice: v.number() }), - v.object({ approved: v.literal(false), reason: v.string() }) + v.object({ approved: v.literal(false), reason: v.string() }), ), }); @@ -25,7 +25,7 @@ export const confirmationWorkflow = workflow.define({ const proposals = await ctx.runAction( internal.userConfirmation.generateProposals, { prompt: args.prompt }, - { retry: true } + { retry: true }, ); console.log("Proposals generated", proposals); const approval = await ctx.awaitEvent(approvalEvent); diff --git a/package.json b/package.json index f371089..6de6877 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "clean": "rm -rf dist *.tsbuildinfo", "build": "tsc --project ./tsconfig.build.json", "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex", - "lint": "eslint src", + "lint": "eslint .", "all": "run-p -r 'dev:*' 'test:watch'", "test": "vitest run --typecheck", "test:watch": "vitest --typecheck --clearScreen false", diff --git a/src/client/environment.test.ts b/src/client/environment.test.ts index e661c6b..d3c9ae1 100644 --- a/src/client/environment.test.ts +++ b/src/client/environment.test.ts @@ -70,7 +70,7 @@ describe("environment patching units", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); expect(DeterministicDate.now()).toBe(testTime); @@ -83,7 +83,7 @@ describe("environment patching units", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const date = new DeterministicDate(); @@ -93,7 +93,7 @@ describe("environment patching units", () => { it("should create Date with provided args", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const date = new DeterministicDate(2023, 0, 1); @@ -105,7 +105,7 @@ describe("environment patching units", () => { it("should return string when called without new", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const dateString = (DeterministicDate as unknown as () => string)(); @@ -116,7 +116,7 @@ describe("environment patching units", () => { const originalDate = Date; const DeterministicDate = createDeterministicDate( originalDate, - mockGetGenerationState + mockGetGenerationState, ); expect(DeterministicDate.parse).toBe(originalDate.parse); @@ -140,7 +140,7 @@ describe("environment patching units", () => { it("should produce identical outputs for specific dates", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); // Test with specific timestamps @@ -169,7 +169,7 @@ describe("environment patching units", () => { expect(deterministic.getMinutes()).toBe(original.getMinutes()); expect(deterministic.getSeconds()).toBe(original.getSeconds()); expect(deterministic.getMilliseconds()).toBe( - original.getMilliseconds() + original.getMilliseconds(), ); } @@ -182,7 +182,7 @@ describe("environment patching units", () => { 10, 30, 45, - 123 + 123, ); expect(deterministic.getFullYear()).toBe(original.getFullYear()); @@ -192,14 +192,14 @@ describe("environment patching units", () => { expect(deterministic.getMinutes()).toBe(original.getMinutes()); expect(deterministic.getSeconds()).toBe(original.getSeconds()); expect(deterministic.getMilliseconds()).toBe( - original.getMilliseconds() + original.getMilliseconds(), ); }); it("should produce identical string representations for deterministic dates", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const timestamp = 1640995200000; // 2022-01-01T00:00:00.000Z @@ -218,7 +218,7 @@ describe("environment patching units", () => { it("should handle UTC methods identically", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const timestamp = 1640995200123; // 2022-01-01T00:00:00.123Z @@ -232,7 +232,7 @@ describe("environment patching units", () => { expect(deterministic.getUTCMinutes()).toBe(original.getUTCMinutes()); expect(deterministic.getUTCSeconds()).toBe(original.getUTCSeconds()); expect(deterministic.getUTCMilliseconds()).toBe( - original.getUTCMilliseconds() + original.getUTCMilliseconds(), ); expect(deterministic.getUTCDay()).toBe(original.getUTCDay()); }); @@ -240,7 +240,7 @@ describe("environment patching units", () => { it("should handle static methods identically", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const dateString = "2023-01-15T10:30:45.123Z"; @@ -253,17 +253,17 @@ describe("environment patching units", () => { const ms = 123; expect(DeterministicDate.parse(dateString)).toBe( - Date.parse(dateString) + Date.parse(dateString), ); expect( - DeterministicDate.UTC(year, month, day, hour, minute, second, ms) + DeterministicDate.UTC(year, month, day, hour, minute, second, ms), ).toBe(Date.UTC(year, month, day, hour, minute, second, ms)); }); it("should maintain Date compatibility", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const date = new DeterministicDate(2023, 0, 1); @@ -282,7 +282,7 @@ describe("environment patching units", () => { it("should handle Date modification methods correctly", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const timestamp = 1640995200000; // 2022-01-01 @@ -307,7 +307,7 @@ describe("environment patching units", () => { it("should have timezone and locale methods available for future patching", () => { const DeterministicDate = createDeterministicDate( Date, - mockGetGenerationState + mockGetGenerationState, ); const date = new DeterministicDate(1640995200000); // 2022-01-01T00:00:00.000Z @@ -375,7 +375,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); proxiedConsole.log("test"); @@ -394,7 +394,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); // Methods should be functions (noop) but not call the original @@ -413,11 +413,11 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); expect(() => proxiedConsole.Console).toThrow( - "console.Console() is not supported within workflows" + "console.Console() is not supported within workflows", ); }); @@ -426,7 +426,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); proxiedConsole.count("test"); @@ -443,7 +443,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); proxiedConsole.count("test"); @@ -461,7 +461,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); proxiedConsole.groupEnd(); @@ -479,7 +479,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); proxiedConsole.time("test"); @@ -493,7 +493,7 @@ describe("environment patching units", () => { const proxiedConsole = createConsole( mockConsole as unknown as Console, - mockGetGenerationState + mockGetGenerationState, ); proxiedConsole.count("test"); diff --git a/src/client/environment.ts b/src/client/environment.ts index d1598a6..80900d1 100644 --- a/src/client/environment.ts +++ b/src/client/environment.ts @@ -46,7 +46,7 @@ export function patchMath(math: typeof Math, seed: string): typeof Math { // Testable unit: creates deterministic Date constructor export function createDeterministicDate( originalDate: typeof Date, - getGenerationState: () => GenerationState + getGenerationState: () => GenerationState, ): typeof Date { function DeterministicDate(this: unknown, ...args: unknown[]) { // `Date()` was called directly, not as a constructor. @@ -59,7 +59,7 @@ export function createDeterministicDate( return new originalDate(now) as unknown as Date; } return new (originalDate as typeof Date)( - ...(args as ConstructorParameters) + ...(args as ConstructorParameters), ) as unknown as Date; } @@ -84,7 +84,7 @@ export function createDeterministicDate( export function setupEnvironment( getGenerationState: () => GenerationState, - workflowId: string + workflowId: string, ): void { const global = globalThis as Record; @@ -101,7 +101,7 @@ export function setupEnvironment( // Patch fetch global.fetch = (_input: RequestInfo | URL, _init?: RequestInit) => { throw new Error( - `Fetch isn't currently supported within workflows. Perform the fetch within an action and call it with step.runAction().` + `Fetch isn't currently supported within workflows. Perform the fetch within an action and call it with step.runAction().`, ); }; @@ -124,7 +124,7 @@ function noop() {} // exported for testing export function createConsole( console: Console, - getGenerationState: () => GenerationState + getGenerationState: () => GenerationState, ): Console { const counts: Record = {}; const times: Record = {}; @@ -152,7 +152,7 @@ export function createConsole( return target[prop]; case "Console": throw new Error( - "console.Console() is not supported within workflows" + "console.Console() is not supported within workflows", ); case "count": return (label?: string) => { diff --git a/src/client/index.ts b/src/client/index.ts index 07601af..c2ced62 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -81,7 +81,7 @@ export type WorkflowDefinition< args?: ArgsValidator; handler: ( step: WorkflowCtx, - args: ObjectType + args: ObjectType, ) => Promise>; returns?: ReturnsValidator; workpoolOptions?: WorkpoolRetryOptions; @@ -98,7 +98,7 @@ export class WorkflowManager { public component: WorkflowComponent, public options?: { workpoolOptions: WorkpoolOptions; - } + }, ) {} /** @@ -111,7 +111,7 @@ export class WorkflowManager { ArgsValidator extends PropertyValidators, ReturnsValidator extends Validator | void, >( - workflow: WorkflowDefinition + workflow: WorkflowDefinition, ): RegisteredMutation< "internal", { @@ -125,7 +125,7 @@ export class WorkflowManager { return workflowMutation( this.component, workflow, - this.options?.workpoolOptions + this.options?.workpoolOptions, ); } @@ -156,7 +156,7 @@ export class WorkflowManager { startAsync?: boolean; /** @deprecated Use `startAsync` instead. */ validateAsync?: boolean; - } + }, ): Promise { const handle = await createFunctionHandle(workflow); const onComplete = options?.onComplete @@ -185,11 +185,11 @@ export class WorkflowManager { */ async status( ctx: RunQueryCtx, - workflowId: WorkflowId + workflowId: WorkflowId, ): Promise { const { workflow, inProgress } = await ctx.runQuery( this.component.workflow.getStatus, - { workflowId } + { workflowId }, ); const running = inProgress.map((entry) => entry.step as IdsToStrings); switch (workflow.runResult?.kind) { @@ -233,7 +233,7 @@ export class WorkflowManager { opts?: { order?: "asc" | "desc"; paginationOpts?: PaginationOptions; - } + }, ): Promise> { const steps = await ctx.runQuery(this.component.workflow.listSteps, { workflowId, @@ -277,7 +277,7 @@ export class WorkflowManager { | { validator?: undefined; value?: T } | { validator: Validator; value: T } | { error: string; value?: undefined } - ) + ), ): Promise> { const result: RunResult = "error" in args @@ -310,7 +310,7 @@ export class WorkflowManager { */ async createEvent( ctx: RunMutationCtx, - args: { name: Name; workflowId: WorkflowId } + args: { name: Name; workflowId: WorkflowId }, ): Promise> { return (await ctx.runMutation(this.component.event.create, { name: args.name, diff --git a/src/client/safeFunctionName.ts b/src/client/safeFunctionName.ts index d852e6e..3497903 100644 --- a/src/client/safeFunctionName.ts +++ b/src/client/safeFunctionName.ts @@ -7,7 +7,7 @@ import { } from "convex/server"; export function safeFunctionName( - f: FunctionReference + f: FunctionReference, ) { const address = getFunctionAddress(f); return ( diff --git a/src/client/step.ts b/src/client/step.ts index 67a6732..9fa2a7e 100644 --- a/src/client/step.ts +++ b/src/client/step.ts @@ -61,11 +61,11 @@ export class StepExecutor { private journalEntries: Array, private receiver: BaseChannel, private now: number, - private workpoolOptions: WorkpoolOptions | undefined + private workpoolOptions: WorkpoolOptions | undefined, ) { this.journalEntrySize = journalEntries.reduce( (size, entry) => size + journalEntrySize(entry), - 0 + 0, ); if (this.journalEntrySize > MAX_JOURNAL_SIZE) { @@ -121,21 +121,21 @@ export class StepExecutor { completeMessage(message: StepRequest, entry: JournalEntry) { if (entry.step.inProgress) { throw new Error( - `Assertion failed: not blocked but have in-progress journal entry` + `Assertion failed: not blocked but have in-progress journal entry`, ); } const stepArgsJson = JSON.stringify(convexToJson(entry.step.args)); const messageArgsJson = JSON.stringify( - convexToJson(message.target.args as Value) + convexToJson(message.target.args as Value), ); if (stepArgsJson !== messageArgsJson) { throw new Error( - `Journal entry mismatch: ${entry.step.args} !== ${message.target.args}` + `Journal entry mismatch: ${entry.step.args} !== ${message.target.args}`, ); } if (entry.step.runResult === undefined) { throw new Error( - `Assertion failed: no outcome for completed function call` + `Assertion failed: no outcome for completed function call`, ); } switch (entry.step.runResult.kind) { @@ -189,7 +189,7 @@ export class StepExecutor { schedulerOptions: message.schedulerOptions, step, }; - }) + }), ); const entries = (await this.ctx.runMutation( this.component.journal.startSteps, @@ -198,14 +198,14 @@ export class StepExecutor { generationNumber: this.generationNumber, steps, workpoolOptions: this.workpoolOptions, - } + }, )) as JournalEntry[]; for (const entry of entries) { this.journalEntrySize += journalEntrySize(entry); if (this.journalEntrySize > MAX_JOURNAL_SIZE) { throw new Error( journalSizeError(this.journalEntrySize, this.workflowId) + - ` The failing step was ${entry.step.name} (${entry._id})` + ` The failing step was ${entry.step.name} (${entry._id})`, ); } } diff --git a/src/client/validator.ts b/src/client/validator.ts index fb93bb9..a94b481 100644 --- a/src/client/validator.ts +++ b/src/client/validator.ts @@ -7,7 +7,7 @@ import { export function checkArgs( args: Value, - validator: PropertyValidators | undefined + validator: PropertyValidators | undefined, ) { if (!validator) { return; @@ -20,7 +20,7 @@ export function checkArgs( function check( value: Value, - validator: GenericValidator + validator: GenericValidator, ): { ok: true } | { ok: false; message: string } { switch (validator.kind) { case "id": { @@ -149,7 +149,7 @@ function check( }; } for (const [field, fieldValue] of Object.entries( - value as Record + value as Record, )) { const keyResult = check(field, validator.key); if (!keyResult.ok) { diff --git a/src/client/workflowContext.ts b/src/client/workflowContext.ts index e0cb9fd..ea6a1aa 100644 --- a/src/client/workflowContext.ts +++ b/src/client/workflowContext.ts @@ -36,7 +36,7 @@ export type WorkflowCtx = { runQuery>( query: Query, args: FunctionArgs, - opts?: RunOptions + opts?: RunOptions, ): Promise>; /** @@ -49,7 +49,7 @@ export type WorkflowCtx = { runMutation>( mutation: Mutation, args: FunctionArgs, - opts?: RunOptions + opts?: RunOptions, ): Promise>; /** @@ -62,7 +62,7 @@ export type WorkflowCtx = { runAction>( action: Action, args: FunctionArgs, - opts?: RunOptions & RetryOption + opts?: RunOptions & RetryOption, ): Promise>; /** @@ -75,7 +75,7 @@ export type WorkflowCtx = { runWorkflow>( workflow: Workflow, args: FunctionArgs["args"], - opts?: RunOptions + opts?: RunOptions, ): Promise>; /** @@ -95,13 +95,13 @@ export type WorkflowCtx = { | { name?: Name; id: EventId } ) & { validator?: Validator; - } + }, ): Promise; }; export function createWorkflowCtx( workflowId: WorkflowId, - sender: BaseChannel + sender: BaseChannel, ) { return { workflowId, @@ -156,7 +156,7 @@ async function runFunction< functionType: FunctionType, f: F, args: unknown, - opts?: RunOptions & RetryOption + opts?: RunOptions & RetryOption, ): Promise { const { name, retry, ...schedulerOptions } = opts ?? {}; return run(sender, { @@ -174,7 +174,7 @@ async function runFunction< async function run( sender: BaseChannel, - request: Omit + request: Omit, ): Promise { let send: unknown; const p = new Promise((resolve, reject) => { diff --git a/src/client/workflowMutation.ts b/src/client/workflowMutation.ts index 91e680f..5afb2a6 100644 --- a/src/client/workflowMutation.ts +++ b/src/client/workflowMutation.ts @@ -34,7 +34,7 @@ const workflowArgs = v.union( v.object({ fn: v.string(), args: v.any(), - }) + }), ); const INVALID_WORKFLOW_MESSAGE = `Invalid arguments for workflow: Did you invoke the workflow with ctx.runMutation() instead of workflow.start()? Pro tip: to start a workflow directly from the CLI or dashboard, you can use args '{ fn: "path/to/file:workflowName", args: { ...your workflow args } }'`; @@ -45,7 +45,7 @@ const INVALID_WORKFLOW_MESSAGE = `Invalid arguments for workflow: Did you invoke export function workflowMutation( component: WorkflowComponent, registered: WorkflowDefinition, - defaultWorkpoolOptions?: WorkpoolOptions + defaultWorkpoolOptions?: WorkpoolOptions, ): RegisteredMutation< "internal", { @@ -76,7 +76,7 @@ export function workflowMutation( const { workflowId, generationNumber } = args; const { workflow, logLevel, journalEntries, ok } = await ctx.runQuery( component.journal.load, - { workflowId, shortCircuit: true } + { workflowId, shortCircuit: true }, ); const inProgress = journalEntries.filter(({ step }) => step.inProgress); const console = createLogger(logLevel); @@ -91,7 +91,7 @@ export function workflowMutation( } if (workflow.generationNumber !== generationNumber) { console.error( - `Invalid generation number: ${generationNumber} running workflow ${workflow.name} (${workflowId})` + `Invalid generation number: ${generationNumber} running workflow ${workflow.name} (${workflowId})`, ); return; } @@ -104,18 +104,18 @@ export function workflowMutation( `Workflow ${workflowId} blocked by ` + inProgress .map((entry) => `${entry.step.name} (${entry._id})`) - .join(", ") + .join(", "), ); return; } for (const journalEntry of journalEntries) { assert( !journalEntry.step.inProgress, - `Assertion failed: not blocked but have in-progress journal entry` + `Assertion failed: not blocked but have in-progress journal entry`, ); } const channel = new BaseChannel( - workpoolOptions.maxParallelism ?? 10 + workpoolOptions.maxParallelism ?? 10, ); const step = createWorkflowCtx(workflowId, channel); const executor = new StepExecutor( @@ -126,7 +126,7 @@ export function workflowMutation( journalEntries as JournalEntry[], channel, Date.now(), - workpoolOptions + workpoolOptions, ); setupEnvironment(executor.getGenerationState.bind(executor), workflowId); @@ -149,7 +149,7 @@ export function workflowMutation( : formatErrorWithStack(error); console.error( "Workflow handler returned invalid return value: ", - message + message, ); runResult = { kind: "failed", diff --git a/src/component/event.ts b/src/component/event.ts index b39f9ec..a002316 100644 --- a/src/component/event.ts +++ b/src/component/event.ts @@ -10,7 +10,7 @@ import { enqueueWorkflow, getWorkpool, workpoolOptions } from "./pool.js"; export async function awaitEvent( ctx: MutationCtx, entry: Doc<"steps">, - args: { eventId?: Id<"events">; name: string } + args: { eventId?: Id<"events">; name: string }, ) { const event = await getOrCreateEvent(ctx, entry.workflowId, args, [ "sent", @@ -19,12 +19,12 @@ export async function awaitEvent( switch (event.state.kind) { case "consumed": { throw new Error( - `Event already consumed: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})` + `Event already consumed: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})`, ); } case "waiting": { throw new Error( - `Event already waiting: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})` + `Event already waiting: ${event._id} (${entry.step.name}) in workflow ${entry.workflowId} step ${entry.stepNumber} (${entry._id})`, ); } } @@ -67,13 +67,13 @@ async function getOrCreateEvent( ctx: MutationCtx, workflowId: Id<"workflows"> | undefined, args: { eventId?: Id<"events">; name?: string }, - statuses: Doc<"events">["state"]["kind"][] + statuses: Doc<"events">["state"]["kind"][], ): Promise> { if (args.eventId) { const event = await ctx.db.get(args.eventId); if (!event) { throw new Error( - `Event not found: ${args.eventId} (${args.name}) in workflow ${workflowId}` + `Event not found: ${args.eventId} (${args.name}) in workflow ${workflowId}`, ); } return event; @@ -84,7 +84,7 @@ async function getOrCreateEvent( const event = await ctx.db .query("events") .withIndex("workflowId_state", (q) => - q.eq("workflowId", workflowId).eq("state.kind", status) + q.eq("workflowId", workflowId).eq("state.kind", status), ) .filter((q) => q.eq(q.field("name"), args.name)) .first(); @@ -117,19 +117,19 @@ export const send = mutation({ eventId: args.eventId, name: args.name, }, - ["waiting", "created"] + ["waiting", "created"], ); const { workflowId } = event; const name = args.name ?? event.name; switch (event.state.kind) { case "sent": { throw new Error( - `Event already sent: ${event._id} (${name}) in workflow ${workflowId}` + `Event already sent: ${event._id} (${name}) in workflow ${workflowId}`, ); } case "consumed": { throw new Error( - `Event already consumed: ${event._id} (${name}) in workflow ${workflowId}` + `Event already consumed: ${event._id} (${name}) in workflow ${workflowId}`, ); } case "created": { @@ -142,7 +142,7 @@ export const send = mutation({ const step = await ctx.db.get(event.state.stepId); assert( step, - `Entry ${event.state.stepId} not found when sending event ${event._id} (${name}) in workflow ${workflowId}` + `Entry ${event.state.stepId} not found when sending event ${event._id} (${name}) in workflow ${workflowId}`, ); assert(step.step.kind === "event", "Step is not an event"); step.step.eventId = event._id; @@ -162,7 +162,7 @@ export const send = mutation({ const anyMoreEvents = await ctx.db .query("events") .withIndex("workflowId_state", (q) => - q.eq("workflowId", workflowId).eq("state.kind", "waiting") + q.eq("workflowId", workflowId).eq("state.kind", "waiting"), ) .order("desc") .first(); diff --git a/src/component/journal.ts b/src/component/journal.ts index 758050d..62aeafd 100644 --- a/src/component/journal.ts +++ b/src/component/journal.ts @@ -45,7 +45,7 @@ export const load = query({ const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => - q.eq("step.inProgress", true).eq("workflowId", workflowId) + q.eq("step.inProgress", true).eq("workflowId", workflowId), ) .first(); if (inProgress) { @@ -82,10 +82,10 @@ export const startSteps = mutation({ schedulerOptions: v.optional( v.union( v.object({ runAt: v.optional(v.number()) }), - v.object({ runAfter: v.optional(v.number()) }) - ) + v.object({ runAfter: v.optional(v.number()) }), + ), ), - }) + }), ), workpoolOptions: v.optional(workpoolOptions), }, @@ -147,7 +147,7 @@ export const startSteps = mutation({ maxParallelism: args.workpoolOptions?.maxParallelism, onComplete: { fnHandle: await createFunctionHandle( - internal.pool.nestedWorkflowOnComplete + internal.pool.nestedWorkflowOnComplete, ), context: { stepId, @@ -171,7 +171,7 @@ export const startSteps = mutation({ ctx, step.handle as FunctionHandle<"query">, step.args, - { context, onComplete, name, ...schedulerOptions } + { context, onComplete, name, ...schedulerOptions }, ); break; } @@ -180,7 +180,7 @@ export const startSteps = mutation({ ctx, step.handle as FunctionHandle<"mutation">, step.args, - { context, onComplete, name, ...schedulerOptions } + { context, onComplete, name, ...schedulerOptions }, ); break; } @@ -189,7 +189,7 @@ export const startSteps = mutation({ ctx, step.handle as FunctionHandle<"action">, step.args, - { context, onComplete, name, retry, ...schedulerOptions } + { context, onComplete, name, retry, ...schedulerOptions }, ); break; } @@ -205,7 +205,7 @@ export const startSteps = mutation({ stepNumber, }); return entry; - }) + }), ); return entries; }, diff --git a/src/component/logging.ts b/src/component/logging.ts index 8ca55e8..18ce9e4 100644 --- a/src/component/logging.ts +++ b/src/component/logging.ts @@ -10,7 +10,7 @@ export const logLevel = v.union( v.literal("INFO"), v.literal("REPORT"), v.literal("WARN"), - v.literal("ERROR") + v.literal("ERROR"), ); export type LogLevel = Infer; @@ -32,7 +32,7 @@ const logLevelByName = logLevelOrder.reduce( acc[l] = i; return acc; }, - {} as Record + {} as Record, ); export function shouldLog(config: LogLevel, level: LogLevel) { return logLevelByName[config] <= logLevelByName[level]; diff --git a/src/component/model.ts b/src/component/model.ts index ec6c3a1..8a5f46e 100644 --- a/src/component/model.ts +++ b/src/component/model.ts @@ -3,7 +3,7 @@ import type { QueryCtx } from "./_generated/server.js"; export async function getWorkflow( ctx: QueryCtx, workflowIdStr: string, - expectedGenerationNumber: number | null + expectedGenerationNumber: number | null, ) { const workflowId = ctx.db.normalizeId("workflows", workflowIdStr); if (!workflowId) { @@ -18,7 +18,7 @@ export async function getWorkflow( workflow.generationNumber !== expectedGenerationNumber ) { throw new Error( - `Invalid generation number: ${expectedGenerationNumber} for workflow ${workflow.name} (${workflowId})` + `Invalid generation number: ${expectedGenerationNumber} for workflow ${workflow.name} (${workflowId})`, ); } return workflow; diff --git a/src/component/pool.ts b/src/component/pool.ts index b72e32e..ef63afb 100644 --- a/src/component/pool.ts +++ b/src/component/pool.ts @@ -42,7 +42,7 @@ export const DEFAULT_RETRY_BEHAVIOR = { export async function getWorkpool( ctx: MutationCtx, - opts: WorkpoolOptions | undefined + opts: WorkpoolOptions | undefined, ) { // nit: can fetch config only if necessary const config = await ctx.db.query("config").first(); @@ -94,7 +94,7 @@ async function onCompleteHandler( workflowId?: WorkflowId; result: RunResult; context: object; - } + }, ) { const console = await getDefaultLogger(ctx); const stepId = @@ -130,13 +130,13 @@ async function onCompleteHandler( const workflow = await getWorkflow(ctx, workflowId, null); if (workflow.generationNumber !== generationNumber) { console.error( - `Workflow: ${workflowId} already has generation number ${workflow.generationNumber} when completing ${stepId}` + `Workflow: ${workflowId} already has generation number ${workflow.generationNumber} when completing ${stepId}`, ); return; } if (!journalEntry.step.inProgress) { console.error( - `Step finished but journal entry not in progress: ${stepId} status: ${journalEntry.step.runResult?.kind ?? "pending"}` + `Step finished but journal entry not in progress: ${stepId} status: ${journalEntry.step.runResult?.kind ?? "pending"}`, ); return; } @@ -175,7 +175,7 @@ async function onCompleteHandler( if (workflow.runResult !== undefined) { if (workflow.runResult.kind !== "canceled") { console.error( - `Workflow: ${workflowId} already ${workflow.runResult.kind} when completing ${stepId} with status ${args.result.kind}` + `Workflow: ${workflowId} already ${workflow.runResult.kind} when completing ${stepId} with status ${args.result.kind}`, ); } return; @@ -187,7 +187,7 @@ async function onCompleteHandler( export async function enqueueWorkflow( ctx: MutationCtx, workflow: Doc<"workflows">, - workpool: Workpool + workpool: Workpool, ) { const { _id: workflowId, generationNumber, name, workflowHandle } = workflow; await workpool.enqueueMutation( @@ -198,7 +198,7 @@ export async function enqueueWorkflow( name, onComplete: internal.pool.handlerOnComplete, context: { workflowId, generationNumber }, - } + }, ); } @@ -233,7 +233,7 @@ export const handlerOnComplete = internalMutation({ console.error("Invalid handlerOnComplete context", args.context); const workflowId = ctx.db.normalizeId( "workflows", - args.context.workflowId + args.context.workflowId, ); await ctx.db.insert("onCompleteFailures", args); if (!workflowId) { diff --git a/src/component/schema.ts b/src/component/schema.ts index 3a12544..0cfdd3c 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -88,7 +88,7 @@ export const step = v.union( ...stepCommonFields, eventId: v.optional(v.id("events")), args: v.object({ eventId: v.optional(v.id("events")) }), - }) + }), ); export type Step = Infer; @@ -167,7 +167,7 @@ export const event = { sentAt: v.number(), consumedAt: v.number(), stepId: v.id("steps"), - }) + }), ), }; @@ -197,7 +197,7 @@ export default defineSchema({ generationNumber: v.number(), runResult: vResultValidator, error: v.string(), - }) - ) + }), + ), ), }); diff --git a/src/component/setup.test.ts b/src/component/setup.test.ts index b47f2e1..8cbd7dd 100644 --- a/src/component/setup.test.ts +++ b/src/component/setup.test.ts @@ -8,7 +8,7 @@ export const modules = import.meta.glob("./**/*.*s"); import componentSchema from "../../node_modules/@convex-dev/workpool/src/component/schema.js"; export { componentSchema }; export const componentModules = import.meta.glob( - "../../node_modules/@convex-dev/workpool/src/component/**/*.ts" + "../../node_modules/@convex-dev/workpool/src/component/**/*.ts", ); export function initConvexTest() { const t = convexTest(schema, modules); diff --git a/src/component/workflow.ts b/src/component/workflow.ts index b242b6a..3f896e0 100644 --- a/src/component/workflow.ts +++ b/src/component/workflow.ts @@ -49,7 +49,7 @@ export const create = mutation({ export async function createHandler( ctx: MutationCtx, args: Infer, - schedulerOptions?: SchedulerOptions + schedulerOptions?: SchedulerOptions, ) { const console = await getDefaultLogger(ctx); await updateMaxParallelism(ctx, console, args.maxParallelism); @@ -63,7 +63,7 @@ export async function createHandler( console.debug( `Created workflow ${workflowId}:`, args.workflowArgs, - args.workflowHandle + args.workflowHandle, ); if (args.startAsync) { const workpool = await getWorkpool(ctx, args); @@ -76,7 +76,7 @@ export async function createHandler( onComplete: internal.pool.handlerOnComplete, context: { workflowId, generationNumber: 0 }, ...schedulerOptions, - } + }, ); } else { // If we can't start it, may as well not create it, eh? Fail fast... @@ -105,7 +105,7 @@ export const getStatus = query({ const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => - q.eq("step.inProgress", true).eq("workflowId", args.workflowId) + q.eq("step.inProgress", true).eq("workflowId", args.workflowId), ) .collect(); console.debug(`${args.workflowId} blocked by`, inProgress); @@ -198,12 +198,12 @@ export const complete = mutation({ // When the overall workflow completes (successfully or not). export async function completeHandler( ctx: MutationCtx, - args: Infer + args: Infer, ) { const workflow = await getWorkflow( ctx, args.workflowId, - args.generationNumber + args.generationNumber, ); const console = await getDefaultLogger(ctx); if (workflow.runResult) { @@ -223,7 +223,7 @@ export async function completeHandler( const inProgress = await ctx.db .query("steps") .withIndex("inProgress", (q) => - q.eq("step.inProgress", true).eq("workflowId", args.workflowId) + q.eq("step.inProgress", true).eq("workflowId", args.workflowId), ) .collect(); if (inProgress.length > 0) { @@ -257,7 +257,7 @@ export async function completeHandler( workflowId: workflow._id as unknown as WorkflowId, result: workflow.runResult, context: workflow.onComplete.context, - } + }, ); } catch (error) { const message = formatErrorWithStack(error); @@ -290,7 +290,7 @@ export const cleanup = mutation({ // TODO: allow cleaning up a workflow from inside it / in the onComplete hook if (!workflow.runResult) { logger.debug( - `Can't clean up workflow ${workflowId} since it hasn't completed.` + `Can't clean up workflow ${workflowId} since it hasn't completed.`, ); return false; } @@ -311,7 +311,7 @@ export const cleanup = mutation({ async function updateMaxParallelism( ctx: MutationCtx, console: Logger, - maxParallelism: number | undefined + maxParallelism: number | undefined, ) { const config = await ctx.db.query("config").first(); if (config) { diff --git a/src/test.ts b/src/test.ts index 4a0f38e..4ae275c 100644 --- a/src/test.ts +++ b/src/test.ts @@ -10,7 +10,7 @@ const modules = import.meta.glob("./component/**/*.ts"); */ export function register( t: TestConvex>, - name: string = "workflow" + name: string = "workflow", ) { t.registerComponent(name, schema, modules); } diff --git a/src/types.ts b/src/types.ts index 59b4a84..f7a7d6d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,7 @@ export const vWorkflowStep = v.object({ kind: v.union( v.literal("function"), v.literal("workflow"), - v.literal("event") + v.literal("event"), ), workId: v.optional(vWorkIdValidator), nestedWorkflowId: v.optional(vWorkflowId), @@ -110,8 +110,8 @@ export function vPaginationResult< v.union( v.literal("SplitRecommended"), v.literal("SplitRequired"), - v.null() - ) + v.null(), + ), ), }); } diff --git a/tsconfig.test.json b/tsconfig.test.json index 51c78a1..aff5230 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -7,9 +7,5 @@ "example/src/**/*.tsx", "example/convex/**/*.ts" ], - "exclude": [ - "node_modules", - "dist", - "**/_generated" - ] + "exclude": ["node_modules", "dist", "**/_generated"] } From 5468ba55c1b72ae58983b583eac5931878521406 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 5 Nov 2025 22:54:03 -0800 Subject: [PATCH 09/20] update codegen --- example/convex/_generated/server.d.ts | 16 ++-- example/convex/_generated/server.js | 13 +-- src/component/_generated/api.js | 23 ----- src/component/_generated/{api.d.ts => api.ts} | 15 ++-- .../{dataModel.d.ts => dataModel.ts} | 0 src/component/_generated/server.js | 90 ------------------- .../_generated/{server.d.ts => server.ts} | 54 ++++++----- 7 files changed, 54 insertions(+), 157 deletions(-) delete mode 100644 src/component/_generated/api.js rename src/component/_generated/{api.d.ts => api.ts} (92%) rename src/component/_generated/{dataModel.d.ts => dataModel.ts} (100%) delete mode 100644 src/component/_generated/server.js rename src/component/_generated/{server.d.ts => server.ts} (79%) diff --git a/example/convex/_generated/server.d.ts b/example/convex/_generated/server.d.ts index b5c6828..bec05e6 100644 --- a/example/convex/_generated/server.d.ts +++ b/example/convex/_generated/server.d.ts @@ -10,7 +10,6 @@ import { ActionBuilder, - AnyComponents, HttpActionBuilder, MutationBuilder, QueryBuilder, @@ -19,15 +18,9 @@ import { GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter, - FunctionReference, } from "convex/server"; import type { DataModel } from "./dataModel.js"; -type GenericCtx = - | GenericActionCtx - | GenericMutationCtx - | GenericQueryCtx; - /** * Define a query in this Convex app's public API. * @@ -92,11 +85,12 @@ export declare const internalAction: ActionBuilder; /** * Define an HTTP action. * - * This function will be used to respond to HTTP requests received by a Convex - * deployment if the requests matches the path and method where this action - * is routed. Be sure to route your action in `convex/http.js`. + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. * - * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export declare const httpAction: HttpActionBuilder; diff --git a/example/convex/_generated/server.js b/example/convex/_generated/server.js index 4a21df4..bf3d25a 100644 --- a/example/convex/_generated/server.js +++ b/example/convex/_generated/server.js @@ -16,7 +16,6 @@ import { internalActionGeneric, internalMutationGeneric, internalQueryGeneric, - componentsGeneric, } from "convex/server"; /** @@ -81,10 +80,14 @@ export const action = actionGeneric; export const internalAction = internalActionGeneric; /** - * Define a Convex HTTP action. + * Define an HTTP action. * - * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object - * as its second. - * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export const httpAction = httpActionGeneric; diff --git a/src/component/_generated/api.js b/src/component/_generated/api.js deleted file mode 100644 index 44bf985..0000000 --- a/src/component/_generated/api.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -/** - * Generated `api` utility. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ - -import { anyApi, componentsGeneric } from "convex/server"; - -/** - * A utility for referencing Convex functions in your app's API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ -export const api = anyApi; -export const internal = anyApi; -export const components = componentsGeneric(); diff --git a/src/component/_generated/api.d.ts b/src/component/_generated/api.ts similarity index 92% rename from src/component/_generated/api.d.ts rename to src/component/_generated/api.ts index 58605fc..8151e93 100644 --- a/src/component/_generated/api.d.ts +++ b/src/component/_generated/api.ts @@ -21,8 +21,9 @@ import type { FilterApi, FunctionReference, } from "convex/server"; +import { anyApi, componentsGeneric } from "convex/server"; -declare const fullApi: ApiFromModules<{ +const fullApi: ApiFromModules<{ event: typeof event; journal: typeof journal; logging: typeof logging; @@ -30,7 +31,7 @@ declare const fullApi: ApiFromModules<{ pool: typeof pool; utils: typeof utils; workflow: typeof workflow; -}>; +}> = anyApi as any; /** * A utility for referencing Convex functions in your app's public API. @@ -40,10 +41,10 @@ declare const fullApi: ApiFromModules<{ * const myFunctionReference = api.myModule.myFunction; * ``` */ -export declare const api: FilterApi< +export const api: FilterApi< typeof fullApi, FunctionReference ->; +> = anyApi as any; /** * A utility for referencing Convex functions in your app's internal API. @@ -53,12 +54,12 @@ export declare const api: FilterApi< * const myFunctionReference = internal.myModule.myFunction; * ``` */ -export declare const internal: FilterApi< +export const internal: FilterApi< typeof fullApi, FunctionReference ->; +> = anyApi as any; -export declare const components: { +export const components = componentsGeneric() as unknown as { workpool: { lib: { cancel: FunctionReference< diff --git a/src/component/_generated/dataModel.d.ts b/src/component/_generated/dataModel.ts similarity index 100% rename from src/component/_generated/dataModel.d.ts rename to src/component/_generated/dataModel.ts diff --git a/src/component/_generated/server.js b/src/component/_generated/server.js deleted file mode 100644 index 4a21df4..0000000 --- a/src/component/_generated/server.js +++ /dev/null @@ -1,90 +0,0 @@ -/* eslint-disable */ -/** - * Generated utilities for implementing server-side Convex query and mutation functions. - * - * THIS CODE IS AUTOMATICALLY GENERATED. - * - * To regenerate, run `npx convex dev`. - * @module - */ - -import { - actionGeneric, - httpActionGeneric, - queryGeneric, - mutationGeneric, - internalActionGeneric, - internalMutationGeneric, - internalQueryGeneric, - componentsGeneric, -} from "convex/server"; - -/** - * Define a query in this Convex app's public API. - * - * This function will be allowed to read your Convex database and will be accessible from the client. - * - * @param func - The query function. It receives a {@link QueryCtx} as its first argument. - * @returns The wrapped query. Include this as an `export` to name it and make it accessible. - */ -export const query = queryGeneric; - -/** - * Define a query that is only accessible from other Convex functions (but not from the client). - * - * This function will be allowed to read from your Convex database. It will not be accessible from the client. - * - * @param func - The query function. It receives a {@link QueryCtx} as its first argument. - * @returns The wrapped query. Include this as an `export` to name it and make it accessible. - */ -export const internalQuery = internalQueryGeneric; - -/** - * Define a mutation in this Convex app's public API. - * - * This function will be allowed to modify your Convex database and will be accessible from the client. - * - * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. - * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. - */ -export const mutation = mutationGeneric; - -/** - * Define a mutation that is only accessible from other Convex functions (but not from the client). - * - * This function will be allowed to modify your Convex database. It will not be accessible from the client. - * - * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. - * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. - */ -export const internalMutation = internalMutationGeneric; - -/** - * Define an action in this Convex app's public API. - * - * An action is a function which can execute any JavaScript code, including non-deterministic - * code and code with side-effects, like calling third-party services. - * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. - * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. - * - * @param func - The action. It receives an {@link ActionCtx} as its first argument. - * @returns The wrapped action. Include this as an `export` to name it and make it accessible. - */ -export const action = actionGeneric; - -/** - * Define an action that is only accessible from other Convex functions (but not from the client). - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument. - * @returns The wrapped function. Include this as an `export` to name it and make it accessible. - */ -export const internalAction = internalActionGeneric; - -/** - * Define a Convex HTTP action. - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object - * as its second. - * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. - */ -export const httpAction = httpActionGeneric; diff --git a/src/component/_generated/server.d.ts b/src/component/_generated/server.ts similarity index 79% rename from src/component/_generated/server.d.ts rename to src/component/_generated/server.ts index b5c6828..24994e4 100644 --- a/src/component/_generated/server.d.ts +++ b/src/component/_generated/server.ts @@ -8,9 +8,8 @@ * @module */ -import { +import type { ActionBuilder, - AnyComponents, HttpActionBuilder, MutationBuilder, QueryBuilder, @@ -19,15 +18,18 @@ import { GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter, - FunctionReference, +} from "convex/server"; +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, } from "convex/server"; import type { DataModel } from "./dataModel.js"; -type GenericCtx = - | GenericActionCtx - | GenericMutationCtx - | GenericQueryCtx; - /** * Define a query in this Convex app's public API. * @@ -36,7 +38,7 @@ type GenericCtx = * @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @returns The wrapped query. Include this as an `export` to name it and make it accessible. */ -export declare const query: QueryBuilder; +export const query: QueryBuilder = queryGeneric; /** * Define a query that is only accessible from other Convex functions (but not from the client). @@ -46,7 +48,8 @@ export declare const query: QueryBuilder; * @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @returns The wrapped query. Include this as an `export` to name it and make it accessible. */ -export declare const internalQuery: QueryBuilder; +export const internalQuery: QueryBuilder = + internalQueryGeneric; /** * Define a mutation in this Convex app's public API. @@ -56,7 +59,7 @@ export declare const internalQuery: QueryBuilder; * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. */ -export declare const mutation: MutationBuilder; +export const mutation: MutationBuilder = mutationGeneric; /** * Define a mutation that is only accessible from other Convex functions (but not from the client). @@ -66,7 +69,8 @@ export declare const mutation: MutationBuilder; * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. */ -export declare const internalMutation: MutationBuilder; +export const internalMutation: MutationBuilder = + internalMutationGeneric; /** * Define an action in this Convex app's public API. @@ -79,7 +83,7 @@ export declare const internalMutation: MutationBuilder; * @param func - The action. It receives an {@link ActionCtx} as its first argument. * @returns The wrapped action. Include this as an `export` to name it and make it accessible. */ -export declare const action: ActionBuilder; +export const action: ActionBuilder = actionGeneric; /** * Define an action that is only accessible from other Convex functions (but not from the client). @@ -87,19 +91,26 @@ export declare const action: ActionBuilder; * @param func - The function. It receives an {@link ActionCtx} as its first argument. * @returns The wrapped function. Include this as an `export` to name it and make it accessible. */ -export declare const internalAction: ActionBuilder; +export const internalAction: ActionBuilder = + internalActionGeneric; /** * Define an HTTP action. * - * This function will be used to respond to HTTP requests received by a Convex - * deployment if the requests matches the path and method where this action - * is routed. Be sure to route your action in `convex/http.js`. + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. * - * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ -export declare const httpAction: HttpActionBuilder; +export const httpAction: HttpActionBuilder = httpActionGeneric; + +type GenericCtx = + | GenericActionCtx + | GenericMutationCtx + | GenericQueryCtx; /** * A set of services for use within Convex query functions. @@ -107,8 +118,7 @@ export declare const httpAction: HttpActionBuilder; * The query context is passed as the first argument to any Convex query * function run on the server. * - * This differs from the {@link MutationCtx} because all of the services are - * read-only. + * If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead. */ export type QueryCtx = GenericQueryCtx; @@ -117,6 +127,8 @@ export type QueryCtx = GenericQueryCtx; * * The mutation context is passed as the first argument to any Convex mutation * function run on the server. + * + * If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead. */ export type MutationCtx = GenericMutationCtx; From 93092112088e3a66cf11a95d20079ea71b0e9105 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 11:51:27 -0800 Subject: [PATCH 10/20] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59cf742..e4c8816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.3.0 + +- Adds /test and /_generated/component.js entrypoints +- Drops commonjs support +- Improves source mapping for generated files +- Changes to a statically generated component API + ## 0.2.8 alpha - Adds asynchronous events - wait for an event in a workflow, send events From 07965c1a811e0369244a1bbd35e538fa5c041877 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 11:53:59 -0800 Subject: [PATCH 11/20] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4c8816..208a428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.3.0 +## 0.2.8-alpha.NaN - Adds /test and /_generated/component.js entrypoints - Drops commonjs support From 0d1c819ab8a654fba5274689b432f3a84f5235fa Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 11:54:55 -0800 Subject: [PATCH 12/20] fancy new codegen --- convex.json | 6 +- example/convex/_generated/api.d.ts | 392 +---------------------------- src/test.ts | 1 + 3 files changed, 7 insertions(+), 392 deletions(-) diff --git a/convex.json b/convex.json index 1704a04..c8dda28 100644 --- a/convex.json +++ b/convex.json @@ -1,3 +1,7 @@ { - "functions": "example/convex" + "functions": "example/convex", + "$schema": "./node_modules/convex/schemas/convex.schema.json", + "codegen": { + "legacyComponentApi": false + } } diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 9a9b4cf..ca3ee05 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -57,395 +57,5 @@ export declare const internal: FilterApi< >; export declare const components: { - workflow: { - event: { - create: FunctionReference< - "mutation", - "internal", - { name: string; workflowId: string }, - string - >; - send: FunctionReference< - "mutation", - "internal", - { - eventId?: string; - name?: string; - result: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - workflowId?: string; - workpoolOptions?: { - defaultRetryBehavior?: { - base: number; - initialBackoffMs: number; - maxAttempts: number; - }; - logLevel?: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - maxParallelism?: number; - retryActionsByDefault?: boolean; - }; - }, - string - >; - }; - journal: { - load: FunctionReference< - "query", - "internal", - { shortCircuit?: boolean; workflowId: string }, - { - blocked?: boolean; - journalEntries: Array<{ - _creationTime: number; - _id: string; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - stepNumber: number; - workflowId: string; - }>; - logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - ok: boolean; - workflow: { - _creationTime: number; - _id: string; - args: any; - generationNumber: number; - logLevel?: any; - name?: string; - onComplete?: { context?: any; fnHandle: string }; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt?: any; - state?: any; - workflowHandle: string; - }; - } - >; - startSteps: FunctionReference< - "mutation", - "internal", - { - generationNumber: number; - steps: Array<{ - retry?: - | boolean - | { base: number; initialBackoffMs: number; maxAttempts: number }; - schedulerOptions?: { runAt?: number } | { runAfter?: number }; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - }>; - workflowId: string; - workpoolOptions?: { - defaultRetryBehavior?: { - base: number; - initialBackoffMs: number; - maxAttempts: number; - }; - logLevel?: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - maxParallelism?: number; - retryActionsByDefault?: boolean; - }; - }, - Array<{ - _creationTime: number; - _id: string; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - stepNumber: number; - workflowId: string; - }> - >; - }; - workflow: { - cancel: FunctionReference< - "mutation", - "internal", - { workflowId: string }, - null - >; - cleanup: FunctionReference< - "mutation", - "internal", - { workflowId: string }, - boolean - >; - complete: FunctionReference< - "mutation", - "internal", - { - generationNumber: number; - runResult: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - workflowId: string; - }, - null - >; - create: FunctionReference< - "mutation", - "internal", - { - maxParallelism?: number; - onComplete?: { context?: any; fnHandle: string }; - startAsync?: boolean; - workflowArgs: any; - workflowHandle: string; - workflowName: string; - }, - string - >; - getStatus: FunctionReference< - "query", - "internal", - { workflowId: string }, - { - inProgress: Array<{ - _creationTime: number; - _id: string; - step: - | { - args: any; - argsSize: number; - completedAt?: number; - functionType: "query" | "mutation" | "action"; - handle: string; - inProgress: boolean; - kind?: "function"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workId?: string; - } - | { - args: any; - argsSize: number; - completedAt?: number; - handle: string; - inProgress: boolean; - kind: "workflow"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - workflowId?: string; - } - | { - args: { eventId?: string }; - argsSize: number; - completedAt?: number; - eventId?: string; - inProgress: boolean; - kind: "event"; - name: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - }; - stepNumber: number; - workflowId: string; - }>; - logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; - workflow: { - _creationTime: number; - _id: string; - args: any; - generationNumber: number; - logLevel?: any; - name?: string; - onComplete?: { context?: any; fnHandle: string }; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt?: any; - state?: any; - workflowHandle: string; - }; - } - >; - listSteps: FunctionReference< - "query", - "internal", - { - order: "asc" | "desc"; - paginationOpts: { - cursor: string | null; - endCursor?: string | null; - id?: number; - maximumBytesRead?: number; - maximumRowsRead?: number; - numItems: number; - }; - workflowId: string; - }, - { - continueCursor: string; - isDone: boolean; - page: Array<{ - args: any; - completedAt?: number; - eventId?: string; - kind: "function" | "workflow" | "event"; - name: string; - nestedWorkflowId?: string; - runResult?: - | { kind: "success"; returnValue: any } - | { error: string; kind: "failed" } - | { kind: "canceled" }; - startedAt: number; - stepId: string; - stepNumber: number; - workId?: string; - workflowId: string; - }>; - pageStatus?: "SplitRecommended" | "SplitRequired" | null; - splitCursor?: string | null; - } - >; - }; - }; + workflow: import("@convex-dev/workflow/_generated/component.js").ComponentApi<"workflow">; }; diff --git a/src/test.ts b/src/test.ts index 4ae275c..737dd80 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,3 +1,4 @@ +/// import type { TestConvex } from "convex-test"; import type { GenericSchema, SchemaDefinition } from "convex/server"; import schema from "./component/schema.js"; From 3777b1f472b0b969be876028167b151c89e51909 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 11:56:20 -0800 Subject: [PATCH 13/20] github test workflow --- .github/workflows/test.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..117b322 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Test and lint +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + push: + branches: [main] + pull_request: + branches: ["**"] + +jobs: + check: + name: Test and lint + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + + - name: Node setup + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + cache-dependency-path: package.json + node-version: "20.x" + cache: "npm" + + - name: Install and build + run: | + npm i + npm run build + - name: Publish package for testing branch + run: npx pkg-pr-new publish || echo "Have you set up pkg-pr-new for this repo?" + - name: Test + run: | + npm run test + npm run typecheck + npm run lint From 4f6668d084763a69f89287ec0a0c91493ac0370d Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 12:04:15 -0800 Subject: [PATCH 14/20] 0.3.0 --- CHANGELOG.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 208a428..3087c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## 0.2.8-alpha.NaN +## 0.3.0 -- Adds /test and /_generated/component.js entrypoints +- Adds /test and /\_generated/component.js entrypoints - Drops commonjs support - Improves source mapping for generated files - Changes to a statically generated component API diff --git a/package-lock.json b/package-lock.json index 41eb8af..1109049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.10", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.10", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 6de6877..f73f37c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.10", + "version": "0.3.0", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 35c92cb425232790705af853af76e765e484005a Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 12:14:53 -0800 Subject: [PATCH 15/20] contributing --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1fd47d..b59c18f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ npm run test ```sh npm run clean -npm run build +npm ci npm pack ``` @@ -33,7 +33,7 @@ npm pack npm run release ``` -#### Alpha release +or for alpha release: ```sh npm run alpha From 0830b2e6c67c58caa06efa697c5928a42720212e Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 18:34:56 -0800 Subject: [PATCH 16/20] update module resolution --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f73f37c..089fab2 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "predev": "npm run dev:backend -- --until-success", "clean": "rm -rf dist *.tsbuildinfo", "build": "tsc --project ./tsconfig.build.json", - "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex", + "typecheck": "tsc --noEmit && tsc -p example/convex", "lint": "eslint .", "all": "run-p -r 'dev:*' 'test:watch'", "test": "vitest run --typecheck", From ac98a4d801c50c097c5864f2a07c1231e8aa4dda Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 18:37:47 -0800 Subject: [PATCH 17/20] remove attw --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 089fab2..3e8a763 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,9 @@ "test:watch": "vitest --typecheck --clearScreen false", "test:debug": "vitest --inspect-brk --no-file-parallelism", "test:coverage": "vitest run --coverage --coverage.reporter=text", - "attw": "attw $(npm pack -s) --exclude-entrypoints ./convex.config --profile esm-only", "prepare": "npm run build", - "alpha": "npm run clean && npm ci && run-p test lint typecheck attw && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", - "release": "npm run clean && npm ci && run-p test lint typecheck attw && npm version patch && npm publish && git push --tags", + "alpha": "npm run clean && npm ci && run-p test lint typecheck && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", + "release": "npm run clean && npm ci && run-p test lint typecheck && npm version patch && npm publish && git push --tags", "version": "pbcopy <<<$npm_package_version; vim CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md" }, "files": [ From 6b725aee1e695bc81485a0e9fef2bab335d5717c Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 18:58:14 -0800 Subject: [PATCH 18/20] drop old config --- .github/workflows/node.js.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/node.js.yml diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index 26591bb..0000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Run tests -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - - name: Use Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 - - run: npm i - - run: npm run build - - run: npx pkg-pr-new publish - - run: npm run typecheck - - run: npm test From 2172793c449ce9afb14e6ed2e14071e86b651cd7 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 6 Nov 2025 19:07:01 -0800 Subject: [PATCH 19/20] add ./convex.config.js entrypoint --- README.md | 2 +- package.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 55fb201..bd24b3b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Then, install the component within your `convex/convex.config.ts` file: ```ts // convex/convex.config.ts -import workflow from "@convex-dev/workflow/convex.config"; +import workflow from "@convex-dev/workflow/convex.config.js"; import { defineApp } from "convex/server"; const app = defineApp(); diff --git a/package.json b/package.json index 3e8a763..5fc7a37 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,10 @@ "./convex.config": { "types": "./dist/component/convex.config.d.ts", "default": "./dist/component/convex.config.js" + }, + "./convex.config.js": { + "types": "./dist/component/convex.config.d.ts", + "default": "./dist/component/convex.config.js" } }, "peerDependencies": { From 664b49526dbea08ac4afa04c1238967bad2e1cae Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Sat, 8 Nov 2025 01:16:27 -0800 Subject: [PATCH 20/20] add lint packages --- package-lock.json | 2 ++ package.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/package-lock.json b/package-lock.json index ba93647..c6e1756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.39.1", + "eslint-plugin-react-hooks": "7.0.1", + "eslint-plugin-react-refresh": "0.4.24", "globals": "16.5.0", "npm-run-all2": "8.0.4", "openai": "6.8.0", diff --git a/package.json b/package.json index 9d32deb..d384867 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.39.1", + "eslint-plugin-react-hooks": "7.0.1", + "eslint-plugin-react-refresh": "0.4.24", "globals": "16.5.0", "npm-run-all2": "8.0.4", "openai": "6.8.0",