diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fae246d..ef973ad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,9 +34,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml registry-url: https://registry.npmjs.org + - name: Install OIDC-capable npm + run: npm install --global npm@11.16.0 - run: pnpm install --frozen-lockfile - id: release run: >- @@ -44,12 +44,10 @@ jobs: --version=${{ inputs.version }} --tag=${{ inputs.dist_tag }} --branch=${{ github.ref_name }} - - name: Verify npm authentication and unused version - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Verify npm registry and unused version run: | set -euo pipefail - if [ -n "${NODE_AUTH_TOKEN:-}" ]; then npm whoami; else npm ping; fi + npm ping if npm view "@rivet-dev/workflows@${{ steps.release.outputs.version }}" version >/dev/null 2>&1; then echo "@rivet-dev/workflows@${{ steps.release.outputs.version }} already exists" >&2 exit 1 @@ -63,7 +61,8 @@ jobs: npm pack ./packages/workflows --silent --pack-destination .pack - name: Publish env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + # setup-node provides a dummy token; clear it so npm uses OIDC. + NODE_AUTH_TOKEN: "" run: >- npm publish .pack/rivet-dev-workflows-${{ steps.release.outputs.version }}.tgz diff --git a/README.md b/README.md index 2f572c7..0ed173e 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,14 @@ pnpm add @rivet-dev/workflows rivetkit ``` ```ts -import { actor } from "rivetkit"; import { workflow } from "@rivet-dev/workflows"; -export const report = actor({ - run: workflow(async (ctx) => { +export const report = workflow({ + run: async (ctx) => { await ctx.step("generate", async (step) => { step.log.info("generating report"); }); - }), + }, }); ``` diff --git a/packages/workflows/README.md b/packages/workflows/README.md index 30a83f7..0e499e7 100644 --- a/packages/workflows/README.md +++ b/packages/workflows/README.md @@ -5,13 +5,12 @@ Durable, replayable workflows for Rivet Actors. [Documentation](https://rivet.dev/workflows/docs) ```ts -import { actor } from "rivetkit"; import { workflow } from "@rivet-dev/workflows"; -export const example = actor({ - run: workflow(async (ctx) => { +export const example = workflow({ + run: async (ctx) => { await ctx.step("hello", async () => "world"); - }), + }, }); ``` diff --git a/packages/workflows/package.json b/packages/workflows/package.json index 662903e..dec4327 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -3,6 +3,11 @@ "version": "2.3.10", "description": "Durable, replayable workflows for Rivet Actors", "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/rivet-dev/workflows.git", + "directory": "packages/workflows" + }, "keywords": [ "rivet", "workflow", @@ -67,7 +72,7 @@ "commander": "^12.0.0", "legacy-rivetkit": "npm:rivetkit@2.3.7", "legacy-workflow-engine": "npm:@rivetkit/workflow-engine@2.3.7", - "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.1550fe4", "tsup": "^8.4.0", "tsx": "^4.7.0", "typescript": "^5.7.3", diff --git a/packages/workflows/src/rivetkit/driver.ts b/packages/workflows/src/rivetkit/driver.ts index e5856c2..a174fd4 100644 --- a/packages/workflows/src/rivetkit/driver.ts +++ b/packages/workflows/src/rivetkit/driver.ts @@ -1,8 +1,5 @@ import type { ActorQueue, ActorRun, RunContext } from "rivetkit"; -import { - WORKFLOW_STORAGE_V1, - type WorkflowStorageHandle, -} from "rivetkit/storage"; +import type { RawAccess } from "rivetkit/db"; import type { EngineDriver, KVEntry, @@ -12,6 +9,14 @@ import type { WorkflowMessageIdentity, } from "../index.js"; +const WORKFLOW_STORAGE_PREFIX = new Uint8Array([6, 1]); +const WORKFLOW_UPSERT_SQL = + "INSERT INTO _rivet_wf_kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"; + +const WORKFLOW_SQLITE_MAX_VALUE_BYTES = 256 * 1024; +const WORKFLOW_SQLITE_MAX_BATCH_ROWS = 128; +const WORKFLOW_SQLITE_MAX_BATCH_BYTES = 512 * 1024; + function track( runCtx: RunContext, promise: Promise, @@ -25,6 +30,169 @@ function track( return promise; } +function prefixWorkflowKey(key: Uint8Array): Uint8Array { + const prefixed = new Uint8Array( + WORKFLOW_STORAGE_PREFIX.byteLength + key.byteLength, + ); + prefixed.set(WORKFLOW_STORAGE_PREFIX); + prefixed.set(key, WORKFLOW_STORAGE_PREFIX.byteLength); + return prefixed; +} + +function stripWorkflowKey(key: Uint8Array): Uint8Array { + if ( + key.byteLength < WORKFLOW_STORAGE_PREFIX.byteLength || + !WORKFLOW_STORAGE_PREFIX.every((byte, index) => key[index] === byte) + ) { + throw new Error("workflow SQLite key escaped the [6, 1] namespace"); + } + return key.slice(WORKFLOW_STORAGE_PREFIX.byteLength); +} + +function computeUpperBound(prefix: Uint8Array): Uint8Array { + const upperBound = prefix.slice(); + for (let index = upperBound.length - 1; index >= 0; index--) { + if (upperBound[index] !== 0xff) { + upperBound[index]++; + return upperBound.slice(0, index + 1); + } + } + + // Every workflow key begins with 6, so a finite upper bound always exists. + throw new Error("workflow storage prefix has no upper bound"); +} + +function normalizeSqlBlob(value: unknown): Uint8Array { + if (value instanceof Uint8Array) { + return value; + } + if (value instanceof ArrayBuffer) { + return new Uint8Array(value); + } + if (ArrayBuffer.isView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + if (Array.isArray(value)) { + const bytes = new Uint8Array(value.length); + for (const [index, byte] of value.entries()) { + if (!Number.isInteger(byte) || byte < 0 || byte > 255) { + throw new Error("workflow SQLite value was not a byte array"); + } + bytes[index] = byte; + } + return bytes; + } + throw new Error("workflow SQLite value was not a blob"); +} + +function validateWrites(writes: KVWrite[]): void { + if (writes.length > WORKFLOW_SQLITE_MAX_BATCH_ROWS) { + throw new Error( + `Workflow batch contains ${writes.length} rows, exceeding the ${WORKFLOW_SQLITE_MAX_BATCH_ROWS} row limit`, + ); + } + + let batchBytes = 0; + for (const write of writes) { + if (write.value.byteLength > WORKFLOW_SQLITE_MAX_VALUE_BYTES) { + throw new Error( + `Workflow value is ${write.value.byteLength} bytes, exceeding the ${WORKFLOW_SQLITE_MAX_VALUE_BYTES} byte limit`, + ); + } + batchBytes += + WORKFLOW_STORAGE_PREFIX.byteLength + + write.key.byteLength + + write.value.byteLength; + } + + if (batchBytes > WORKFLOW_SQLITE_MAX_BATCH_BYTES) { + throw new Error( + `Workflow batch is ${batchBytes} bytes, exceeding the ${WORKFLOW_SQLITE_MAX_BATCH_BYTES} byte limit`, + ); + } +} + +class WorkflowStorage { + #db: RawAccess; + + constructor(db: RawAccess) { + this.#db = db; + } + + async get(key: Uint8Array): Promise { + const rows = await this.#db.execute<{ value: unknown }>( + "SELECT value FROM _rivet_wf_kv WHERE key = ?", + prefixWorkflowKey(key), + ); + const value = rows[0]?.value; + return value == null ? null : normalizeSqlBlob(value); + } + + async set(key: Uint8Array, value: Uint8Array): Promise { + await this.batch([{ key, value }], false); + } + + async delete(key: Uint8Array): Promise { + await this.#db.execute( + "DELETE FROM _rivet_wf_kv WHERE key = ?", + prefixWorkflowKey(key), + ); + } + + async deletePrefix(prefix: Uint8Array): Promise { + const start = prefixWorkflowKey(prefix); + await this.#db.execute( + "DELETE FROM _rivet_wf_kv WHERE key >= ? AND key < ?", + start, + computeUpperBound(start), + ); + } + + async deleteRange(start: Uint8Array, end: Uint8Array): Promise { + await this.#db.execute( + "DELETE FROM _rivet_wf_kv WHERE key >= ? AND key < ?", + prefixWorkflowKey(start), + prefixWorkflowKey(end), + ); + } + + async list(prefix: Uint8Array): Promise { + const start = prefixWorkflowKey(prefix); + const rows = await this.#db.execute<{ key: unknown; value: unknown }>( + "SELECT key, value FROM _rivet_wf_kv WHERE key >= ? AND key < ? ORDER BY key ASC", + start, + computeUpperBound(start), + ); + return rows.map((row) => ({ + key: stripWorkflowKey(normalizeSqlBlob(row.key)), + value: normalizeSqlBlob(row.value), + })); + } + + async batch(writes: KVWrite[], includeState: boolean): Promise { + if (writes.length === 0) return; + validateWrites(writes); + + const commit = async (tx: RawAccess) => { + for (const write of writes) { + await tx.execute( + WORKFLOW_UPSERT_SQL, + prefixWorkflowKey(write.key), + write.value, + ); + } + }; + + if (includeState) { + await this.#db.transaction(commit, { + experimental: { includeState: true }, + }); + } else { + await this.#db.transaction(commit); + } + } +} + class ActorWorkflowMessageDriver implements WorkflowMessageDriver { #runCtx: RunContext; #queue: ActorQueue; @@ -95,7 +263,7 @@ export class ActorWorkflowDriver implements EngineDriver { readonly workerPollInterval = 100; readonly messageDriver: WorkflowMessageDriver; #runCtx: RunContext; - #storage: WorkflowStorageHandle; + #storage: WorkflowStorage; #queue: ActorQueue; #run: ActorRun; @@ -103,7 +271,7 @@ export class ActorWorkflowDriver implements EngineDriver { this.#runCtx = runCtx; this.messageDriver = new ActorWorkflowMessageDriver(runCtx); this.#queue = runCtx.queue; - this.#storage = runCtx.storage.open(WORKFLOW_STORAGE_V1); + this.#storage = new WorkflowStorage(runCtx.db); this.#run = runCtx.run; } @@ -132,9 +300,7 @@ export class ActorWorkflowDriver implements EngineDriver { } async batch(writes: KVWrite[]): Promise { - if (writes.length === 0) return; - - await track(this.#runCtx, this.#storage.flushWithState(writes)); + await track(this.#runCtx, this.#storage.batch(writes, true)); } async setAlarm(_workflowId: string, wakeAt: number): Promise { @@ -184,11 +350,11 @@ export class ActorWorkflowControlDriver implements EngineDriver { readonly workerPollInterval = 100; readonly messageDriver: WorkflowMessageDriver = new NoopWorkflowMessageDriver(); - #storage: WorkflowStorageHandle; + #storage: WorkflowStorage; #run: ActorRun; constructor(runCtx: RunContext) { - this.#storage = runCtx.storage.open(WORKFLOW_STORAGE_V1); + this.#storage = new WorkflowStorage(runCtx.db); this.#run = runCtx.run; } @@ -217,11 +383,7 @@ export class ActorWorkflowControlDriver implements EngineDriver { } async batch(writes: KVWrite[]): Promise { - if (writes.length === 0) { - return; - } - - await this.#storage.batch(writes); + await this.#storage.batch(writes, false); } async setAlarm(_workflowId: string, wakeAt: number): Promise { diff --git a/packages/workflows/src/rivetkit/inspector.ts b/packages/workflows/src/rivetkit/inspector.ts index 0055452..1421b3b 100644 --- a/packages/workflows/src/rivetkit/inspector.ts +++ b/packages/workflows/src/rivetkit/inspector.ts @@ -1,9 +1,9 @@ -import * as transport from "rivetkit/inspector/workflow"; +import * as transport from "rivetkit/experimental/inspector/workflow"; import { encodeWorkflowHistoryTransport, encodeWorkflowInspectorValue, type WorkflowInspectorAdapter, -} from "rivetkit/inspector/workflow"; +} from "rivetkit/experimental/inspector/workflow"; import type { BranchStatus, BranchStatusType, diff --git a/packages/workflows/src/rivetkit/mod.ts b/packages/workflows/src/rivetkit/mod.ts index e424f71..a216d19 100644 --- a/packages/workflows/src/rivetkit/mod.ts +++ b/packages/workflows/src/rivetkit/mod.ts @@ -1,4 +1,8 @@ import { + type Actions, + type ActorConfigInput, + type ActorDefinition, + actor, defineRunHandler, type EventSchemaConfig, type QueueSchemaConfig, @@ -6,7 +10,6 @@ import { type RunContext, type RunControl, } from "rivetkit"; -import type { AnyDatabaseProvider } from "rivetkit/db"; import { isActorAbortedError } from "rivetkit/errors"; import { stringifyError } from "rivetkit/utils"; import { @@ -104,76 +107,125 @@ function isRunHandlerUnavailable(error: unknown): boolean { ); } -export interface WorkflowOptions< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase extends AnyDatabaseProvider, +type DistributiveOmit = T extends unknown + ? Omit + : never; + +export type WorkflowActorConfig< + TState = undefined, + TConnParams = undefined, + TConnState = undefined, + TVars = undefined, + TInput = undefined, TEvents extends EventSchemaConfig = Record, TQueues extends QueueSchemaConfig = Record, -> { - onError?: ( - ctx: RunContext< + TActions extends Actions< + TState, + TConnParams, + TConnState, + TVars, + TInput, + undefined, + TEvents, + TQueues + > = Record, +> = DistributiveOmit< + ActorConfigInput< + TState, + TConnParams, + TConnState, + TVars, + TInput, + undefined, + TEvents, + TQueues, + TActions + >, + "run" | "db" +> & { + run: ( + ctx: WorkflowContext< TState, TConnParams, TConnState, TVars, TInput, - TDatabase, + undefined, TEvents, TQueues >, - event: WorkflowErrorEvent, - ) => void | Promise; -} - -export function workflow< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase extends AnyDatabaseProvider, - TEvents extends EventSchemaConfig = Record, - TQueues extends QueueSchemaConfig = Record, ->( - fn: ( - ctx: WorkflowContext< + ) => Promise; + onError?: ( + ctx: RunContext< TState, TConnParams, TConnState, TVars, TInput, - TDatabase, + undefined, TEvents, TQueues >, - ) => Promise, - options: WorkflowOptions< + event: WorkflowErrorEvent, + ) => void | Promise; +}; + +export function workflow< + TState = undefined, + TConnParams = undefined, + TConnState = undefined, + TVars = undefined, + TInput = undefined, + TEvents extends EventSchemaConfig = Record, + TQueues extends QueueSchemaConfig = Record, + TActions extends Actions< TState, TConnParams, TConnState, TVars, TInput, - TDatabase, + undefined, TEvents, TQueues - > = {}, -): ( - c: RunContext< + > = Record, +>( + config: WorkflowActorConfig< TState, TConnParams, TConnState, TVars, TInput, - TDatabase, TEvents, - TQueues - >, -) => Promise { - const onError = options.onError; + TQueues, + Actions< + TState, + TConnParams, + TConnState, + TVars, + TInput, + undefined, + TEvents, + TQueues + > + > & { actions?: TActions }, +): ActorDefinition< + TState, + TConnParams, + TConnState, + TVars, + TInput, + undefined, + TEvents, + TQueues, + TActions +> { + if (Object.hasOwn(config, "db")) { + throw new TypeError( + "workflow() does not support a custom database provider", + ); + } + + const { run: workflowRun, onError, ...actorConfig } = config; const workflowInspectors = new Map< string, ReturnType @@ -188,7 +240,6 @@ export function workflow< } return workflowInspector; } - async function run( runCtx: RunContext< TState, @@ -196,7 +247,7 @@ export function workflow< TConnState, TVars, TInput, - TDatabase, + undefined, TEvents, TQueues >, @@ -242,7 +293,7 @@ export function workflow< const handle = runWorkflow( runCtx.actorId, - async (ctx) => await fn(new WorkflowContext(ctx, runCtx)), + async (ctx) => await workflowRun(new WorkflowContext(ctx, runCtx)), undefined, driver, { @@ -295,7 +346,7 @@ export function workflow< } } - return defineRunHandler(run, { + const runHandler = defineRunHandler(run, { icon: "diagram-project", inspectorKind: "workflow", createInspector: ({ actorId, control }) => { @@ -314,4 +365,19 @@ export function workflow< }; }, }); + + return actor< + TState, + TConnParams, + TConnState, + TVars, + TInput, + undefined, + TEvents, + TQueues, + TActions + >({ + ...actorConfig, + run: runHandler, + }); } diff --git a/packages/workflows/tests/e2e/preview-runtime.test.ts b/packages/workflows/tests/e2e/preview-runtime.test.ts index 2543061..d4525f5 100644 --- a/packages/workflows/tests/e2e/preview-runtime.test.ts +++ b/packages/workflows/tests/e2e/preview-runtime.test.ts @@ -1,11 +1,11 @@ -import { actor, setup } from "rivetkit"; +import { setup } from "rivetkit"; import { setupTest } from "rivetkit/test"; import { expect, test } from "vitest"; import { workflow } from "../../src/rivetkit/mod"; -const sleepAcrossWake = actor({ +const sleepAcrossWake = workflow({ state: { completed: [] as string[] }, - run: workflow(async (ctx) => { + run: async (ctx) => { await ctx.step("before-sleep", async (step) => { step.state.completed.push("before-sleep"); }); @@ -13,7 +13,7 @@ const sleepAcrossWake = actor({ await ctx.step("after-sleep", async (step) => { step.state.completed.push("after-sleep"); }); - }), + }, actions: { getCompleted: (ctx) => ctx.state.completed, }, diff --git a/packages/workflows/tests/fixtures/rivetkit-db.ts b/packages/workflows/tests/fixtures/rivetkit-db.ts new file mode 100644 index 0000000..8d433d9 --- /dev/null +++ b/packages/workflows/tests/fixtures/rivetkit-db.ts @@ -0,0 +1,80 @@ +import { vi } from "vitest"; + +function compareBytes(a: Uint8Array, b: Uint8Array): number { + for (let index = 0; index < Math.min(a.length, b.length); index++) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return a.length - b.length; +} + +function keyOf(key: Uint8Array): string { + return Buffer.from(key).toString("hex"); +} + +export function createTestDatabase() { + const rows = new Map(); + + const execute = vi.fn( + async ( + sql: string, + ...args: unknown[] + ): Promise[]> => { + if (sql.startsWith("INSERT INTO _rivet_wf_kv")) { + const [key, value] = args as [Uint8Array, Uint8Array]; + rows.set(keyOf(key), { key, value }); + return []; + } + + if (sql === "SELECT value FROM _rivet_wf_kv WHERE key = ?") { + const row = rows.get(keyOf(args[0] as Uint8Array)); + return row ? [{ value: row.value }] : []; + } + + if (sql.startsWith("SELECT key, value FROM _rivet_wf_kv")) { + const [start, end] = args as [Uint8Array, Uint8Array]; + return [...rows.values()] + .filter( + (row) => + compareBytes(row.key, start) >= 0 && + compareBytes(row.key, end) < 0, + ) + .sort((a, b) => compareBytes(a.key, b.key)); + } + + if (sql === "DELETE FROM _rivet_wf_kv WHERE key = ?") { + rows.delete(keyOf(args[0] as Uint8Array)); + return []; + } + + if (sql === "DELETE FROM _rivet_wf_kv WHERE key >= ? AND key < ?") { + const [start, end] = args as [Uint8Array, Uint8Array]; + for (const [mapKey, row] of rows) { + if ( + compareBytes(row.key, start) >= 0 && + compareBytes(row.key, end) < 0 + ) { + rows.delete(mapKey); + } + } + return []; + } + + throw new Error(`Unexpected workflow SQL: ${sql}`); + }, + ); + + const db = { + execute, + transaction: vi.fn( + async ( + callback: (tx: { + execute: typeof execute; + }) => unknown | Promise, + _options?: unknown, + ) => await callback(db), + ), + close: vi.fn(async () => {}), + }; + + return { db, rows }; +} diff --git a/packages/workflows/tests/fixtures/rivetkit-storage.ts b/packages/workflows/tests/fixtures/rivetkit-storage.ts deleted file mode 100644 index 91d7ecc..0000000 --- a/packages/workflows/tests/fixtures/rivetkit-storage.ts +++ /dev/null @@ -1 +0,0 @@ -export const WORKFLOW_STORAGE_V1 = "rivetkit.workflow-storage.v1"; diff --git a/packages/workflows/tests/fixtures/rivetkit.ts b/packages/workflows/tests/fixtures/rivetkit.ts index b0c9dc5..2fb327e 100644 --- a/packages/workflows/tests/fixtures/rivetkit.ts +++ b/packages/workflows/tests/fixtures/rivetkit.ts @@ -2,6 +2,17 @@ type AnyFunction = (...args: any[]) => any; const optionsByRun = new WeakMap(); +export function actor(config: TConfig): { config: TConfig } { + return { config }; +} + +export function queue(): { + readonly _queueMessage?: TMessage; + readonly _queueComplete?: TComplete; +} { + return {}; +} + export class RivetError extends Error { group: string; code: string; diff --git a/packages/workflows/tests/rivetkit/driver.test.ts b/packages/workflows/tests/rivetkit/driver.test.ts index daf6128..83b755b 100644 --- a/packages/workflows/tests/rivetkit/driver.test.ts +++ b/packages/workflows/tests/rivetkit/driver.test.ts @@ -1,6 +1,9 @@ -import { WORKFLOW_STORAGE_V1 } from "rivetkit/storage"; import { describe, expect, test, vi } from "vitest"; -import { ActorWorkflowDriver } from "../../src/rivetkit/driver"; +import { + ActorWorkflowControlDriver, + ActorWorkflowDriver, +} from "../../src/rivetkit/driver"; +import { createTestDatabase } from "../fixtures/rivetkit-db"; function write(key = 1, value = 2) { return { @@ -10,16 +13,7 @@ function write(key = 1, value = 2) { } function createSubject() { - const storage = { - get: vi.fn(async () => null), - set: vi.fn(async () => {}), - delete: vi.fn(async () => {}), - deletePrefix: vi.fn(async () => {}), - deleteRange: vi.fn(async () => {}), - list: vi.fn(async () => []), - batch: vi.fn(async () => {}), - flushWithState: vi.fn(async () => {}), - }; + const { db } = createTestDatabase(); const waitUntil: Promise[] = []; const queue = { send: vi.fn(async () => {}), @@ -28,42 +22,118 @@ function createSubject() { waitForAvailable: vi.fn(async () => {}), }; const run = { setWakeAt: vi.fn(async () => {}) }; - const open = vi.fn(() => storage); const ctx = { - storage: { open }, + db, queue, run, waitUntil: (promise: Promise) => waitUntil.push(promise), }; return { driver: new ActorWorkflowDriver(ctx as never), - storage, + controlDriver: new ActorWorkflowControlDriver(ctx as never), + db, queue, run, - open, waitUntil, }; } describe("RivetKit workflow driver", () => { - test("opens only the opaque workflow storage capability", () => { - const { open } = createSubject(); - expect(open).toHaveBeenCalledOnce(); - expect(open).toHaveBeenCalledWith(WORKFLOW_STORAGE_V1); + test("stores the existing workflow rows under the [6, 1] namespace", async () => { + const { driver, db } = createSubject(); + await driver.batch([write(3, 4)]); + + const insert = db.execute.mock.calls.find(([sql]) => + String(sql).startsWith("INSERT INTO _rivet_wf_kv"), + ); + expect(insert?.[1]).toEqual(new Uint8Array([6, 1, 3])); + expect(insert?.[2]).toEqual(new Uint8Array([4])); + }); + + test("commits live workflow writes with actor state", async () => { + const { driver, db } = createSubject(); + await driver.batch([write(), write(3, 4)]); + expect(db.transaction).toHaveBeenCalledOnce(); + expect(db.transaction.mock.calls[0]?.[1]).toEqual({ + experimental: { includeState: true }, + }); + }); + + test("uses an ordinary transaction for control writes", async () => { + const { controlDriver, db } = createSubject(); + await controlDriver.batch([write()]); + expect(db.transaction).toHaveBeenCalledOnce(); + expect(db.transaction.mock.calls[0]?.[1]).toBeUndefined(); + }); + + test("reads, lists, and deletes byte-compatible rows", async () => { + const { driver } = createSubject(); + await driver.batch([write(2, 20), write(1, 10), write(3, 30)]); + + await expect(driver.get(new Uint8Array([2]))).resolves.toEqual( + new Uint8Array([20]), + ); + await expect(driver.list(new Uint8Array())).resolves.toEqual([ + write(1, 10), + write(2, 20), + write(3, 30), + ]); + + await driver.deleteRange(new Uint8Array([1]), new Uint8Array([3])); + await expect(driver.list(new Uint8Array())).resolves.toEqual([ + write(3, 30), + ]); + await driver.deletePrefix(new Uint8Array([3])); + await expect(driver.list(new Uint8Array())).resolves.toEqual([]); }); - test("flushes actor state and the full workflow batch atomically", async () => { - const { driver, storage } = createSubject(); - const writes = [write(), write(3, 4)]; - await driver.batch(writes); - expect(storage.flushWithState).toHaveBeenCalledWith(writes); - expect(storage.batch).not.toHaveBeenCalled(); + test("rejects rows outside the [6, 1] namespace", async () => { + const { driver, db } = createSubject(); + db.execute.mockResolvedValueOnce([ + { key: new Uint8Array([6, 2, 1]), value: new Uint8Array([1]) }, + ]); + await expect(driver.list(new Uint8Array())).rejects.toThrow( + "workflow SQLite key escaped the [6, 1] namespace", + ); }); - test("does not flush an empty batch", async () => { - const { driver, storage } = createSubject(); + test("does not open a transaction for an empty batch", async () => { + const { driver, db } = createSubject(); await driver.batch([]); - expect(storage.flushWithState).not.toHaveBeenCalled(); + expect(db.transaction).not.toHaveBeenCalled(); + }); + + test("rejects values above 256 KiB", async () => { + const { driver, db } = createSubject(); + await expect( + driver.batch([ + { + key: new Uint8Array([1]), + value: new Uint8Array(256 * 1024 + 1), + }, + ]), + ).rejects.toThrow("exceeding the 262144 byte limit"); + expect(db.transaction).not.toHaveBeenCalled(); + }); + + test("rejects batches above 128 rows", async () => { + const { driver, db } = createSubject(); + await expect( + driver.batch(Array.from({ length: 129 }, (_, key) => write(key, 1))), + ).rejects.toThrow("exceeding the 128 row limit"); + expect(db.transaction).not.toHaveBeenCalled(); + }); + + test("rejects batches above 512 KiB", async () => { + const { driver, db } = createSubject(); + const value = new Uint8Array((512 * 1024) / 2); + await expect( + driver.batch([ + { key: new Uint8Array([1]), value }, + { key: new Uint8Array([2]), value }, + ]), + ).rejects.toThrow("exceeding the 524288 byte limit"); + expect(db.transaction).not.toHaveBeenCalled(); }); test("uses the logical run wake source for set and clear", async () => { @@ -96,8 +166,8 @@ describe("RivetKit workflow driver", () => { }); test("tracks host operations with outcome-swallowed waitUntil promises", async () => { - const { driver, storage, waitUntil } = createSubject(); - storage.get.mockRejectedValueOnce(new Error("read failed")); + const { driver, db, waitUntil } = createSubject(); + db.execute.mockRejectedValueOnce(new Error("read failed")); await expect(driver.get(new Uint8Array([1]))).rejects.toThrow( "read failed", ); diff --git a/packages/workflows/tests/rivetkit/types.test.ts b/packages/workflows/tests/rivetkit/types.test.ts new file mode 100644 index 0000000..d747437 --- /dev/null +++ b/packages/workflows/tests/rivetkit/types.test.ts @@ -0,0 +1,58 @@ +import { type AnyActorDefinition, queue } from "rivetkit"; +import type { ActorHandle } from "rivetkit/client"; +import type { RawAccess } from "rivetkit/db"; +import { describe, expectTypeOf, test } from "vitest"; +import { + type WorkflowContextOf, + type WorkflowStepContextOf, + workflow, +} from "../../src/rivetkit/mod"; + +const definition = workflow({ + state: { count: 0 }, + queues: { + jobs: queue<{ id: string }>(), + }, + actions: { + increment: (ctx, amount: number) => { + ctx.state.count += amount; + return ctx.state.count; + }, + }, + run: async (ctx) => { + await ctx.step("typed", async (step) => { + expectTypeOf(step.state.count).toEqualTypeOf(); + expectTypeOf(step.db).toEqualTypeOf(); + await step.queue.send("jobs", { id: "one" }); + }); + }, +}); + +type HasWorkflowAction = + ActorHandle extends { + increment: (...args: any[]) => any; + } + ? true + : false; + +function customDatabaseIsRejected() { + workflow({ + // @ts-expect-error Workflows requires RivetKit's standard embedded database. + db: {}, + run: async () => {}, + }); +} + +describe("workflow actor types", () => { + test("returns a normal actor definition", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf(definition).toMatchTypeOf(); + expectTypeOf< + WorkflowContextOf["actorId"] + >().toEqualTypeOf(); + expectTypeOf< + WorkflowStepContextOf["state"] + >().toEqualTypeOf<{ count: number }>(); + expectTypeOf(customDatabaseIsRejected).toBeFunction(); + }); +}); diff --git a/packages/workflows/tests/rivetkit/workflow.test.ts b/packages/workflows/tests/rivetkit/workflow.test.ts index 4a49c84..278ec95 100644 --- a/packages/workflows/tests/rivetkit/workflow.test.ts +++ b/packages/workflows/tests/rivetkit/workflow.test.ts @@ -1,56 +1,10 @@ import { describe, expect, test, vi } from "vitest"; import { workflow } from "../../src/rivetkit/mod"; import { getDefinedRunHandlerOptions } from "../fixtures/rivetkit"; - -function compareBytes(a: Uint8Array, b: Uint8Array): number { - for (let index = 0; index < Math.min(a.length, b.length); index++) { - if (a[index] !== b[index]) return a[index] - b[index]; - } - return a.length - b.length; -} - -function startsWith(key: Uint8Array, prefix: Uint8Array): boolean { - return prefix.every((byte, index) => key[index] === byte); -} - -type Write = { key: Uint8Array; value: Uint8Array }; +import { createTestDatabase } from "../fixtures/rivetkit-db"; function createRunContext() { - const rows = new Map(); - const keyOf = (key: Uint8Array) => Buffer.from(key).toString("hex"); - const apply = (writes: Write[]) => { - for (const write of writes) { - rows.set(keyOf(write.key), write); - } - }; - const storage = { - get: async (key: Uint8Array) => rows.get(keyOf(key))?.value ?? null, - set: async (key: Uint8Array, value: Uint8Array) => apply([{ key, value }]), - delete: async (key: Uint8Array) => { - rows.delete(keyOf(key)); - }, - deletePrefix: async (prefix: Uint8Array) => { - for (const [key, row] of rows) { - if (startsWith(row.key, prefix)) rows.delete(key); - } - }, - deleteRange: async (start: Uint8Array, end: Uint8Array) => { - for (const [key, row] of rows) { - if ( - compareBytes(row.key, start) >= 0 && - compareBytes(row.key, end) < 0 - ) { - rows.delete(key); - } - } - }, - list: async (prefix: Uint8Array) => - [...rows.values()] - .filter((row) => startsWith(row.key, prefix)) - .sort((a, b) => compareBytes(a.key, b.key)), - batch: async (writes: Write[]) => apply(writes), - flushWithState: async (writes: Write[]) => apply(writes), - }; + const { db, rows } = createTestDatabase(); const waitUntil: Promise[] = []; const setWakeAt = vi.fn(async () => {}); return { @@ -68,7 +22,7 @@ function createRunContext() { child: () => undefined, }, abortSignal: new AbortController().signal, - storage: { open: () => storage }, + db, run: { setWakeAt }, queue: { send: async () => {}, @@ -85,11 +39,30 @@ function createRunContext() { } describe("workflow RivetKit integration", () => { - test("publishes static Inspector metadata and disposes actor state", async () => { + test("returns an actor definition, forwards config, and disposes Inspector state", async () => { const step = vi.fn(async () => "done"); - const run = workflow(async (ctx) => { - await ctx.step("once", step); + const definition = workflow({ + state: { count: 0 }, + actions: { + getCount: (ctx) => ctx.state.count, + }, + options: { sleepTimeout: 250 }, + run: async (ctx) => { + await ctx.step("once", step); + }, }); + expect(definition).toEqual({ + config: expect.objectContaining({ + state: { count: 0 }, + actions: expect.any(Object), + options: { sleepTimeout: 250 }, + run: expect.any(Function), + }), + }); + const run = definition.config.run; + if (typeof run !== "function") { + throw new Error("workflow actor did not install a run handler"); + } const options = getDefinedRunHandlerOptions(run); expect(options.inspectorKind).toBe("workflow"); @@ -122,4 +95,13 @@ describe("workflow RivetKit integration", () => { }); expect(nextRegistration.inspector.workflow).not.toBe(firstAdapter); }); + + test("rejects a custom database provider", () => { + expect(() => + workflow({ + run: async () => {}, + db: {}, + } as never), + ).toThrow("workflow() does not support a custom database provider"); + }); }); diff --git a/packages/workflows/vitest.config.ts b/packages/workflows/vitest.config.ts index 757dd74..3da1501 100644 --- a/packages/workflows/vitest.config.ts +++ b/packages/workflows/vitest.config.ts @@ -20,13 +20,7 @@ export default defineConfig({ ), }, { - find: "rivetkit/storage", - replacement: fileURLToPath( - new URL("./tests/fixtures/rivetkit-storage.ts", import.meta.url), - ), - }, - { - find: "rivetkit/inspector/workflow", + find: "rivetkit/experimental/inspector/workflow", replacement: fileURLToPath( new URL( "./tests/fixtures/rivetkit-inspector-workflow.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ed8865..ce645aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ importers: specifier: npm:@rivetkit/workflow-engine@2.3.7 version: '@rivetkit/workflow-engine@2.3.7' rivetkit: - specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 - version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1) + specifier: 0.0.0-feat-workflows-public-host-apis.1550fe4 + version: 0.0.0-feat-workflows-public-host-apis.1550fe4(better-sqlite3@12.11.1) tsup: specifier: ^8.4.0 version: 8.5.1(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0) @@ -484,6 +484,13 @@ packages: hono: '>=4.10.0' zod: ^4.0.0 + '@hono/zod-openapi@1.6.1': + resolution: {integrity: sha512-z1xS3FZxl4bBkU3kMIsQsLtsqxPTha5XZCsDEPMSZ6+3RRBQI7KED1GeoDpq2WueXCacsNIjcB0MtCnEHY7ahQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + hono: '>=4.10.0' + zod: ^4.0.0 + '@hono/zod-validator@0.9.0': resolution: {integrity: sha512-n0ZSXmCiHVIp4Y5wlOOyZCeTd/rsawA/qW1cipB8QOYKZ9N8Tk0nZUZCXho9cu374AN4JpDNKioNKBJ/W+LBug==} peerDependencies: @@ -535,8 +542,8 @@ packages: resolution: {integrity: sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg==} engines: {node: ^14.18.0 || >=16.0.0} - '@rivetkit/engine-cli-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-hb/GOOgEPKEZicdBVhnkjIalYZwIx+x0AL3YORl2nXFpMxqWaUtOCGzQB3E2IveSfnPw9SJMKO2xkBe0Z6pjxg==} + '@rivetkit/engine-cli-darwin-arm64@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-MTbiOkotK5lfu3U0sPawrZvcU9ImMNul2hmHgl/3eWJxo7lrvbwq47Eh9AN0g1PSquG1tnwENgIVxV0D9cI0Eg==} engines: {node: '>= 20.0.0'} cpu: [arm64] os: [darwin] @@ -547,8 +554,8 @@ packages: cpu: [arm64] os: [darwin] - '@rivetkit/engine-cli-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-qt128grSxT3p2nSK3uudl1ALh79lzXuQ9IbwIkqXQV8i7TFSF7N9jY98U8FvDI2360whr3qq1lvWmFTGDnJ6/A==} + '@rivetkit/engine-cli-darwin-x64@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-3ZtmB9kn4LPLa4GFGNXbrFIJt+3w1/3OE/q67NIX71+QSGiiIqOfiVDomhEI8CWc+m5TXl9bpuNrR1gb/x0dxA==} engines: {node: '>= 20.0.0'} cpu: [x64] os: [darwin] @@ -559,8 +566,8 @@ packages: cpu: [x64] os: [darwin] - '@rivetkit/engine-cli-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-J9xWBqUvy1D1IWsWpIdkujlg83GqSBdTOTC1DBGPhoQ8It8BSZlRGYvrFLSAFPYqgb1vMv6eFPtipPLPTXecUg==} + '@rivetkit/engine-cli-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-ZZiVwFbk3uewKr2pvRxFtjOwmfhgmr1MJIdPcAJiC45gqGUbYS08Q6dwmXROl/kDy+Md5dSRoNxxAHYNRN0LQg==} engines: {node: '>= 20.0.0'} cpu: [arm64] os: [linux] @@ -571,8 +578,8 @@ packages: cpu: [arm64] os: [linux] - '@rivetkit/engine-cli-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-ImXsYTJnmxwrlOa0DSKVy4+2Dp39a2/FVUzi9jgjz4KWVfR/QafGc/gVntZDIkVSvfTUkG/WW6pp8qS7qrz1oA==} + '@rivetkit/engine-cli-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-tBMnqAIEr1CcPBTZBVhXm6/i8Oisax0U1DFJz3WSzpziYbeRkwmQNIk19hLr2aEmT9tEYtEmtGaKx1bUlbjC2w==} engines: {node: '>= 20.0.0'} cpu: [x64] os: [linux] @@ -589,16 +596,16 @@ packages: cpu: [x64] os: [win32] - '@rivetkit/engine-cli@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-EZE/e48WufHAJFm/0qKzTXY5LKNH0+YAlD26haDhwyucW2OgomdVSdmP2i+1a1wTwqSdtRsQRN7O4DmWSbDRTg==} + '@rivetkit/engine-cli@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-4sX1lGT85JX59xzZL0FOrzBoydm0TmtN4fiVrDsh0AhW5i40Dz2yXFm4DLXcDqSzGH2Ak1uJIygRovRPTyxNmg==} engines: {node: '>= 20.0.0'} '@rivetkit/engine-cli@2.3.7': resolution: {integrity: sha512-CezLwJ0B7dWDbA7qM6Aq04mwnrJAdrDActRrrcb4NBa20h7wO9KPAYBwyJz/dRnKm9EUUNcFZ6hrSDpp6T3+Rg==} engines: {node: '>= 20.0.0'} - '@rivetkit/engine-envoy-protocol@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-ZEzFU2dtHD+IzM57NlmKH5YXl6sHJTPLYBWYwgfGH1fJsYRdk8zY6yCSLPkzsCcKC8418XIY01mgaymd0vRDPA==} + '@rivetkit/engine-envoy-protocol@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-OPc3ycQYt0ClNPPaOmQHtpB/1rUTHHIeZP545hUsR4pFpDNpWehnxa/ozoT4DTHnX1kcAXGQ1UfPRx1Vq2OLbw==} '@rivetkit/engine-envoy-protocol@2.3.7': resolution: {integrity: sha512-lsJM3ERwozQCebaOMKJzqIQlcLbbN3LXurRB0+7LsM6FUUL2l6hMoq5I2De/G35DJ6OWDva9AmzfvzI+5f1GPg==} @@ -607,8 +614,8 @@ packages: resolution: {integrity: sha512-QBN/KRBXLJdCgN4gBTL3XAc/zKm58atSnieXWMOyFSPmo6F1/yIVV/LTRdvAktfCttrGx7W6c32i/lwqCHWnsQ==} engines: {node: '>=20'} - '@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-XHnxDGrdzZGfNdo7kLBUcXua84bqJ/6v/GhZF1s4sajFO1hkWzBGrAq2R3+t8g3tUuimoLxMGfCtO1ctXUBOlg==} + '@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-P3AZ+/aAae/qe/uz5BK1gLuCpkL1daC4GBaqBgaDNVt0RoEygSYV09dbNtdS5ZaP2tknT4k3SiMwYBUgicI0UA==} engines: {node: '>= 20.0.0'} cpu: [arm64] os: [darwin] @@ -619,8 +626,8 @@ packages: cpu: [arm64] os: [darwin] - '@rivetkit/rivetkit-napi-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-ni79/MJRNR/jO/blbvjW+lPhHGFtp6otDSoWmxW/QkeUvFF6wvwBTeNcvR7Bx+GohgU9JC7Y3BOXJGuYrCRbAA==} + '@rivetkit/rivetkit-napi-darwin-x64@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-KHZofYS2KMlrtfMdRmW0nG1BBwDhlGIOAisyhz2+YSSGZxrHa3M+RQe0F2LAJtHQMNnuI6EkusqPuBVw5LyCpg==} engines: {node: '>= 20.0.0'} cpu: [x64] os: [darwin] @@ -631,8 +638,8 @@ packages: cpu: [x64] os: [darwin] - '@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-uUHF7vF5N8pegVtluIzix3xYBTik25cL9izjMhwAzoL2/+2UvQaqMg47hqgO1hwr17ImY7UV/WcwOyNODzEJHg==} + '@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-UgXT77vT7PjepfHNbPaWgwy1foMsThZ2MfsY9neRS5DV9ofrWExg924G6HQ81gwTY4rlRyokjaH1B5ISX4wf3A==} engines: {node: '>= 20.0.0'} cpu: [arm64] os: [linux] @@ -643,8 +650,8 @@ packages: cpu: [arm64] os: [linux] - '@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-7/BCrybtr1Tl3GJ1JXyG9MxxsCmfREgpVYDm0H4MZdgu8Lckc7V2CQWWwuXK1bgSewCPcD1QLsVMVj6qhBdxyQ==} + '@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-ADfyga+4Nvh5vqrgaPVFaV04QChH0Ttt1pO1IAuZ0m1r38c1PhvD3rYndQzQvJk1uKUDWQy3/tDEgLM3712IGA==} engines: {node: '>= 20.0.0'} cpu: [arm64] os: [linux] @@ -655,8 +662,8 @@ packages: cpu: [arm64] os: [linux] - '@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-SjoGcPPM6L4LFEmc9JvdqCJWMUTBMtkaZDquIiu6kBU2bB7V///fqRtMTX06oASdZoU4P7CTSyEGg1DPS2RACg==} + '@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-ynTOHBnihwt6lLBYvwZ1zhN9y0CoSfb9IMjnshz60f0p2BF/SRK2Z3umjydcTegHHrwd30Raxwg9ALP+SKgfrg==} engines: {node: '>= 20.0.0'} cpu: [x64] os: [linux] @@ -667,8 +674,8 @@ packages: cpu: [x64] os: [linux] - '@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-yfVy21Fcw6heDNBbI6vICAm4aFQUbs2jSMUexxEXFh5huHjVCHeTLDh9d6TRBB82iDm94i8URU7q/UYYJRzqBQ==} + '@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-0lg3LkT392VoCpTBwkVzJvtHC5sMDLydMCCkHGTScBvVzn3NgG5ILPTfcNiufmJzPzMcNXH77Aji2ed4JRgsmA==} engines: {node: '>= 20.0.0'} cpu: [x64] os: [linux] @@ -685,36 +692,36 @@ packages: cpu: [x64] os: [win32] - '@rivetkit/rivetkit-napi@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-JNsbSqJNqNBT4Meh/CaHA5kJjKokAGfDC0k1E4N2faysbt1zn6LYYQoM12irwj/6++Wr5RpNgOiiDT8npB9d2g==} + '@rivetkit/rivetkit-napi@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-7RS0mXek3iPkSyOXMLIfvXQM6hO0+tVXrmkkxVd7ES16sMm3l4tFydVyr5pkkVJCFpFiRxGVhFIHRO6kDd/Jhw==} engines: {node: '>= 20.0.0'} '@rivetkit/rivetkit-napi@2.3.7': resolution: {integrity: sha512-1d3HtNKzJwdkznWDWSpCwg1rBn+nVwUfcKrApINTfiEhfDyVeznwbVFkfHWqOBHsjU17vvWFfjjTtf8QsjLwUQ==} engines: {node: '>= 20.0.0'} - '@rivetkit/rivetkit-wasm@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-sajvCywErQwBxeIKDbNKp83hjlfUAe8JsFjGz17AiBV+Cgobc6DZOZBLrnItrccJjBtL3Iq0XdOYd60JAF/LkA==} + '@rivetkit/rivetkit-wasm@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-dgvhv5CyU+dKNqPfLn5SXvBdkKsDRILGweqGqcLSN/bpq8jqqhCh2lsdyrOsxtPsH3SaRiAGltbVJIsib0fj5g==} '@rivetkit/rivetkit-wasm@2.3.7': resolution: {integrity: sha512-o7QBtFOJrajyPVBIDQ32k8sDNFsUc8v2ETfNpr+Ch374ydmZMzCv3XKJU9vubc9bZa7WpCHSzf5ztc8b8i/Xpg==} - '@rivetkit/traces@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-TRffcxSyfJE6mooduSxVfnY0JP26ealeLcTeB5MnkqhbbiTY3UV2YG4eJ/mbUKmNWTYQCAtddHGwN42geLzIgQ==} + '@rivetkit/traces@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-FD0n7nq+Lr5jyhbyEenXTtLqDZgCivtL7K+r9x7OpXzgaWUSYtNQy52aEmyq7Tkn4Eb96R5Qyx9DH6ld9kzOuA==} engines: {node: '>=18.0.0'} '@rivetkit/traces@2.3.7': resolution: {integrity: sha512-B7QYjFP2HPxyfPsVjfcNIV7FPLWgGWGypmlT/dZM1i1Cf1WRdeuBumPLffgs3YceiY7Pm4D/gZZVt5p0m4Cx+A==} engines: {node: '>=18.0.0'} - '@rivetkit/virtual-websocket@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-FZBDcOvdg/ywND8KT/lHidfvWsKu1/7RFm1Blc0ULOpp3+788H2NX4zyHvOuF71L8QWfjt9fz1EUm+H87zbhxA==} + '@rivetkit/virtual-websocket@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-5HTt7AoJWGk8jYI2aIHSckLd80mt1jjIxbp4zvPlLfOCd9wyIKstUZABhLyDj0v3rh8mBznwGiGffjKMg88g1Q==} '@rivetkit/virtual-websocket@2.3.7': resolution: {integrity: sha512-jwxDvYbr3YB6vFuxwRdB/AqUUMwdtC1rIAc4tNN6VQR3weHbE+PrDoHKbe85zrtlBtcnG89/p8tlovrj8qyQWA==} - '@rivetkit/workflow-engine@0.0.0-feat-workflows-public-host-apis.0ff6164': - resolution: {integrity: sha512-yjOCVmv67cq66vLeIndsx+jlKo1ct+Tlye1E65O2Xzbc7OcAKVAw/SLOKeBsB4dBMVwTe5Fau4ueuS4xVT3axA==} + '@rivetkit/workflow-engine@0.0.0-feat-workflows-public-host-apis.1550fe4': + resolution: {integrity: sha512-4zAeBLBC5Nh0OVSfm7j/SF9KSN4BOfEDO50bOpgDpKZ+lMohRQ0VX9Ob7Yq036B3T9JIlGVcW4gv9gs9coK9AQ==} engines: {node: '>=18.0.0'} '@rivetkit/workflow-engine@2.3.7': @@ -1375,6 +1382,10 @@ packages: resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==} engines: {node: '>=16.9.0'} + hono@4.13.4: + resolution: {integrity: sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==} + engines: {node: '>=16.9.0'} + https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} @@ -1731,8 +1742,8 @@ packages: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} - rivetkit@0.0.0-feat-workflows-public-host-apis.0ff6164: - resolution: {integrity: sha512-oFSxfTtFdvOiW4ufuGr3liEzy3gXtx08VIqL6LpcbsLdKmg4ivfr48GUGTRpgt+PcmqtmJ/2L90ucHxRK/r83Q==} + rivetkit@0.0.0-feat-workflows-public-host-apis.1550fe4: + resolution: {integrity: sha512-3wGq2SHOfMbBphCNup/JutzdeMjugYhE12j+3ZFPGroox/KtMrrfij3412DrpoZkGP5MMWyYBbnffLmI6kBz0Q==} engines: {node: '>=22.0.0'} peerDependencies: drizzle-kit: ^0.31.2 @@ -2330,11 +2341,24 @@ snapshots: openapi3-ts: 4.6.1 zod: 4.4.3 + '@hono/zod-openapi@1.6.1(hono@4.13.4)(zod@4.4.3)': + dependencies: + '@asteasolutions/zod-to-openapi': 9.1.0(zod@4.4.3) + '@hono/zod-validator': 0.9.0(hono@4.13.4)(zod@4.4.3) + hono: 4.13.4 + openapi3-ts: 4.6.1 + zod: 4.4.3 + '@hono/zod-validator@0.9.0(hono@4.13.3)(zod@4.4.3)': dependencies: hono: 4.13.3 zod: 4.4.3 + '@hono/zod-validator@0.9.0(hono@4.13.4)(zod@4.4.3)': + dependencies: + hono: 4.13.4 + zod: 4.4.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2379,25 +2403,25 @@ snapshots: '@rivetkit/bare-ts@0.6.2': {} - '@rivetkit/engine-cli-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/engine-cli-darwin-arm64@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/engine-cli-darwin-arm64@2.3.7': optional: true - '@rivetkit/engine-cli-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/engine-cli-darwin-x64@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/engine-cli-darwin-x64@2.3.7': optional: true - '@rivetkit/engine-cli-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/engine-cli-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/engine-cli-linux-arm64-musl@2.3.7': optional: true - '@rivetkit/engine-cli-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/engine-cli-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/engine-cli-linux-x64-musl@2.3.7': @@ -2406,12 +2430,12 @@ snapshots: '@rivetkit/engine-cli-win32-x64@2.3.7': optional: true - '@rivetkit/engine-cli@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/engine-cli@0.0.0-feat-workflows-public-host-apis.1550fe4': optionalDependencies: - '@rivetkit/engine-cli-darwin-arm64': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/engine-cli-darwin-x64': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/engine-cli-linux-arm64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/engine-cli-linux-x64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-cli-darwin-arm64': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/engine-cli-darwin-x64': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/engine-cli-linux-arm64-musl': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/engine-cli-linux-x64-musl': 0.0.0-feat-workflows-public-host-apis.1550fe4 '@rivetkit/engine-cli@2.3.7': optionalDependencies: @@ -2421,7 +2445,7 @@ snapshots: '@rivetkit/engine-cli-linux-x64-musl': 2.3.7 '@rivetkit/engine-cli-win32-x64': 2.3.7 - '@rivetkit/engine-envoy-protocol@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/engine-envoy-protocol@0.0.0-feat-workflows-public-host-apis.1550fe4': dependencies: '@rivetkit/bare-ts': 0.6.2 @@ -2431,37 +2455,37 @@ snapshots: '@rivetkit/on-change@6.0.1': {} - '@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/rivetkit-napi-darwin-arm64@2.3.7': optional: true - '@rivetkit/rivetkit-napi-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi-darwin-x64@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/rivetkit-napi-darwin-x64@2.3.7': optional: true - '@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/rivetkit-napi-linux-arm64-gnu@2.3.7': optional: true - '@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/rivetkit-napi-linux-arm64-musl@2.3.7': optional: true - '@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/rivetkit-napi-linux-x64-gnu@2.3.7': optional: true - '@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.1550fe4': optional: true '@rivetkit/rivetkit-napi-linux-x64-musl@2.3.7': @@ -2470,17 +2494,17 @@ snapshots: '@rivetkit/rivetkit-napi-win32-x64-msvc@2.3.7': optional: true - '@rivetkit/rivetkit-napi@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/rivetkit-napi@0.0.0-feat-workflows-public-host-apis.1550fe4': dependencies: '@napi-rs/cli': 2.18.4 - '@rivetkit/engine-envoy-protocol': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-envoy-protocol': 0.0.0-feat-workflows-public-host-apis.1550fe4 optionalDependencies: - '@rivetkit/rivetkit-napi-darwin-arm64': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/rivetkit-napi-darwin-x64': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/rivetkit-napi-linux-arm64-gnu': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/rivetkit-napi-linux-arm64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/rivetkit-napi-linux-x64-gnu': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/rivetkit-napi-linux-x64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi-darwin-arm64': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/rivetkit-napi-darwin-x64': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/rivetkit-napi-linux-arm64-gnu': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/rivetkit-napi-linux-arm64-musl': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/rivetkit-napi-linux-x64-gnu': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/rivetkit-napi-linux-x64-musl': 0.0.0-feat-workflows-public-host-apis.1550fe4 '@rivetkit/rivetkit-napi@2.3.7': dependencies: @@ -2495,11 +2519,11 @@ snapshots: '@rivetkit/rivetkit-napi-linux-x64-musl': 2.3.7 '@rivetkit/rivetkit-napi-win32-x64-msvc': 2.3.7 - '@rivetkit/rivetkit-wasm@0.0.0-feat-workflows-public-host-apis.0ff6164': {} + '@rivetkit/rivetkit-wasm@0.0.0-feat-workflows-public-host-apis.1550fe4': {} '@rivetkit/rivetkit-wasm@2.3.7': {} - '@rivetkit/traces@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/traces@0.0.0-feat-workflows-public-host-apis.1550fe4': dependencies: '@rivetkit/bare-ts': 0.6.2 cbor-x: 1.6.5 @@ -2513,11 +2537,11 @@ snapshots: fdb-tuple: 1.0.0 vbare: 0.0.4 - '@rivetkit/virtual-websocket@0.0.0-feat-workflows-public-host-apis.0ff6164': {} + '@rivetkit/virtual-websocket@0.0.0-feat-workflows-public-host-apis.1550fe4': {} '@rivetkit/virtual-websocket@2.3.7': {} - '@rivetkit/workflow-engine@0.0.0-feat-workflows-public-host-apis.0ff6164': + '@rivetkit/workflow-engine@0.0.0-feat-workflows-public-host-apis.1550fe4': dependencies: '@rivetkit/bare-ts': 0.6.2 cbor-x: 1.6.5 @@ -3167,6 +3191,8 @@ snapshots: hono@4.13.3: {} + hono@4.13.4: {} + https-browserify@1.0.0: {} ieee754@1.2.1: {} @@ -3556,22 +3582,22 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 - rivetkit@0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1): + rivetkit@0.0.0-feat-workflows-public-host-apis.1550fe4(better-sqlite3@12.11.1): dependencies: - '@hono/zod-openapi': 1.6.0(hono@4.13.3)(zod@4.4.3) + '@hono/zod-openapi': 1.6.1(hono@4.13.4)(zod@4.4.3) '@rivet-dev/agent-os-core': 0.1.1 '@rivetkit/bare-ts': 0.6.2 - '@rivetkit/engine-cli': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/engine-envoy-protocol': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-cli': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/engine-envoy-protocol': 0.0.0-feat-workflows-public-host-apis.1550fe4 '@rivetkit/on-change': 6.0.1 - '@rivetkit/rivetkit-napi': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/rivetkit-wasm': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/traces': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/virtual-websocket': 0.0.0-feat-workflows-public-host-apis.0ff6164 - '@rivetkit/workflow-engine': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/rivetkit-wasm': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/traces': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/virtual-websocket': 0.0.0-feat-workflows-public-host-apis.1550fe4 + '@rivetkit/workflow-engine': 0.0.0-feat-workflows-public-host-apis.1550fe4 cbor-x: 1.6.5 drizzle-orm: 0.44.7(better-sqlite3@12.11.1) - hono: 4.13.3 + hono: 4.13.4 invariant: 2.2.4 p-retry: 6.2.1 pino: 9.14.0 diff --git a/scripts/check-boundaries.ts b/scripts/check-boundaries.ts index 3a2309c..6663dea 100644 --- a/scripts/check-boundaries.ts +++ b/scripts/check-boundaries.ts @@ -8,10 +8,19 @@ const forbidden = [ "ACTOR_CONTEXT_INTERNAL_SYMBOL", "RUN_FUNCTION_CONFIG_SYMBOL", "AnyStaticActorInstance", - "_rivet_wf_kv", - "makeWorkflowKey", - "workflowStoragePrefix", "@rivetkit/workflow-engine", + "rivetkit/storage", + "rivetkit/inspector/workflow", + "ctx.storage", + "WORKFLOW_STORAGE_V1", + "flushWithState", + "ctx.sql", + "runCtx.sql", + "_rivet_runtime", + "_rivet_meta", + "_rivet_queue", + "CREATE TABLE _rivet_wf_kv", + "ALTER TABLE _rivet_wf_kv", ]; async function files(path: string): Promise { diff --git a/scripts/verify-pack.ts b/scripts/verify-pack.ts index cdca863..df60663 100644 --- a/scripts/verify-pack.ts +++ b/scripts/verify-pack.ts @@ -75,6 +75,10 @@ try { "RUN_FUNCTION_CONFIG_SYMBOL", "AnyStaticActorInstance", "@rivetkit/workflow-engine", + "rivetkit/storage", + "WORKFLOW_STORAGE_V1", + "flushWithState", + "ctx.sql", ]) { if (source.includes(token)) { throw new Error(`${declaration} exposes private token ${token}`); @@ -121,16 +125,19 @@ try { await writeFile( join(fixture, "smoke.ts"), [ - 'import { actor } from "rivetkit";', 'import { workflow } from "@rivet-dev/workflows";', 'import { InMemoryDriver } from "@rivet-dev/workflows/testing";', - "const definition = actor({", - " run: workflow(async (ctx) => {", + "const definition = workflow({", + " state: { count: 0 },", + " actions: { getCount: (c) => c.state.count },", + " run: async (ctx) => {", ' await ctx.step("typed", async (step) => {', ' step.log.info("compiled");', + ' await step.db.execute("SELECT 1");', + " step.state.count++;", " return 1;", " });", - " }),", + " },", "});", "void definition; void new InMemoryDriver();", ].join("\n"),