@mokronos/wfkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/authoring.d.ts +7 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +8 -0
- package/dist/authoring.js.map +1 -0
- package/dist/cli/integrations.d.ts +3 -0
- package/dist/cli/integrations.d.ts.map +1 -0
- package/dist/cli/integrations.js +40 -0
- package/dist/cli/integrations.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +591 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core.d.ts +198 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1061 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +154 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +24 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +3 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +28 -0
- package/dist/replay.js.map +1 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +183 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +19 -0
- package/dist/schema.js.map +1 -0
- package/dist/schemas.d.ts +1198 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +383 -0
- package/dist/schemas.js.map +1 -0
- package/dist/sdk/artifact.d.ts +41 -0
- package/dist/sdk/artifact.d.ts.map +1 -0
- package/dist/sdk/artifact.js +94 -0
- package/dist/sdk/artifact.js.map +1 -0
- package/dist/sdk/graph.d.ts +15 -0
- package/dist/sdk/graph.d.ts.map +1 -0
- package/dist/sdk/graph.js +388 -0
- package/dist/sdk/graph.js.map +1 -0
- package/dist/sdk/index.d.ts +14 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/integrations.d.ts +18 -0
- package/dist/sdk/integrations.d.ts.map +1 -0
- package/dist/sdk/integrations.js +20 -0
- package/dist/sdk/integrations.js.map +1 -0
- package/dist/sdk/json.d.ts +4 -0
- package/dist/sdk/json.d.ts.map +1 -0
- package/dist/sdk/json.js +31 -0
- package/dist/sdk/json.js.map +1 -0
- package/dist/sdk/loader.d.ts +10 -0
- package/dist/sdk/loader.d.ts.map +1 -0
- package/dist/sdk/loader.js +75 -0
- package/dist/sdk/loader.js.map +1 -0
- package/dist/sdk/sdk.d.ts +102 -0
- package/dist/sdk/sdk.d.ts.map +1 -0
- package/dist/sdk/sdk.js +669 -0
- package/dist/sdk/sdk.js.map +1 -0
- package/dist/sdk/sqlite.d.ts +8 -0
- package/dist/sdk/sqlite.d.ts.map +1 -0
- package/dist/sdk/sqlite.js +288 -0
- package/dist/sdk/sqlite.js.map +1 -0
- package/dist/signal.d.ts +17 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +78 -0
- package/dist/signal.js.map +1 -0
- package/dist/testing/index.d.ts +35 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +244 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +67 -0
- package/src/authoring.ts +32 -0
- package/src/cli/integrations.ts +75 -0
- package/src/cli/main.ts +793 -0
- package/src/core.ts +1515 -0
- package/src/errors.ts +19 -0
- package/src/events.ts +37 -0
- package/src/index.ts +109 -0
- package/src/replay.ts +29 -0
- package/src/runtime.ts +314 -0
- package/src/schema.ts +34 -0
- package/src/schemas.ts +473 -0
- package/src/sdk/artifact.ts +176 -0
- package/src/sdk/graph.ts +486 -0
- package/src/sdk/index.ts +64 -0
- package/src/sdk/integrations.ts +49 -0
- package/src/sdk/json.ts +40 -0
- package/src/sdk/loader.ts +99 -0
- package/src/sdk/sdk.ts +911 -0
- package/src/sdk/sqlite.ts +405 -0
- package/src/signal.ts +116 -0
- package/src/testing/index.ts +341 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
// A typed, tagged workflow error. The returned value is both:
|
|
4
|
+
// - a class you `throw new MyError({...})` inside a step's `run`
|
|
5
|
+
// - a Schema the engine uses to (de)serialize the error across replays
|
|
6
|
+
//
|
|
7
|
+
// Because errors cross the durable boundary they MUST be declared this way
|
|
8
|
+
// (not plain `Error`), so the engine can persist and rehydrate them.
|
|
9
|
+
export const defineError = <
|
|
10
|
+
Tag extends string,
|
|
11
|
+
Fields extends Schema.Struct.Fields
|
|
12
|
+
>(
|
|
13
|
+
tag: Tag,
|
|
14
|
+
fields: Fields
|
|
15
|
+
) => {
|
|
16
|
+
// The self-referential generic of `Schema.TaggedErrorClass` is awkward to thread
|
|
17
|
+
// through a generic factory; the cast is purely to satisfy that phantom type.
|
|
18
|
+
return Schema.TaggedErrorClass<any>()(tag, fields) as any
|
|
19
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Context, Effect } from "effect"
|
|
2
|
+
import { WorkflowEvent as WorkflowEventSchema, isWorkflowEvent } from "./schemas.ts"
|
|
3
|
+
|
|
4
|
+
export { isWorkflowEvent }
|
|
5
|
+
export const WorkflowEvent = WorkflowEventSchema
|
|
6
|
+
export type WorkflowEvent = typeof WorkflowEventSchema.Type
|
|
7
|
+
|
|
8
|
+
export type WorkflowEventSink = (event: WorkflowEvent) => void | Promise<void>
|
|
9
|
+
|
|
10
|
+
export const currentWorkflowEventSink = Context.Reference<WorkflowEventSink | undefined>(
|
|
11
|
+
"wf/currentWorkflowEventSink",
|
|
12
|
+
{ defaultValue: () => undefined }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Durable-engine executions run on entity fibers created when the engine
|
|
16
|
+
// layer is built, so they do not inherit the caller's context reference. Events that
|
|
17
|
+
// carry an executionId are routed through this registry instead.
|
|
18
|
+
const executionEventSinks = new Map<string, WorkflowEventSink>()
|
|
19
|
+
|
|
20
|
+
export const setExecutionEventSink = (executionId: string, sink: WorkflowEventSink): void => {
|
|
21
|
+
executionEventSinks.set(executionId, sink)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const removeExecutionEventSink = (executionId: string): void => {
|
|
25
|
+
executionEventSinks.delete(executionId)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const emitWorkflowEvent = (event: WorkflowEvent): Effect.Effect<void> =>
|
|
29
|
+
Effect.flatMap(currentWorkflowEventSink, (fiberSink) => {
|
|
30
|
+
const executionId = (event as { readonly executionId?: string }).executionId
|
|
31
|
+
const sink = (executionId !== undefined ? executionEventSinks.get(executionId) : undefined) ?? fiberSink
|
|
32
|
+
if (sink === undefined) {
|
|
33
|
+
return Effect.void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Effect.promise(() => Promise.resolve(sink(event)))
|
|
37
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The workflow authoring surface. An authored workflow imports ONLY from here
|
|
2
|
+
// (plus pure helper functions it defines) — never `effect` or `@effect/*`.
|
|
3
|
+
export { createInMemoryDeterminismState, defineStep, defineWorkflow, envSecretResolver, isSecretRef, NonDeterminismError, secret } from "./core.ts"
|
|
4
|
+
export type {
|
|
5
|
+
DefinedWorkflow,
|
|
6
|
+
InMemoryDeterminismState,
|
|
7
|
+
OrchestrationCall,
|
|
8
|
+
OrchestrationKind,
|
|
9
|
+
SecretRef,
|
|
10
|
+
SecretResolver,
|
|
11
|
+
SignalOutcome,
|
|
12
|
+
Step,
|
|
13
|
+
StepConcurrency,
|
|
14
|
+
StepContext,
|
|
15
|
+
StepRetryPolicy,
|
|
16
|
+
TerminalFailure,
|
|
17
|
+
WorkflowContext,
|
|
18
|
+
WorkflowValue
|
|
19
|
+
} from "./core.ts"
|
|
20
|
+
export { defineError } from "./errors.ts"
|
|
21
|
+
export type { WorkflowEvent, WorkflowEventSink } from "./events.ts"
|
|
22
|
+
export {
|
|
23
|
+
ExecutionId,
|
|
24
|
+
JsonSchema,
|
|
25
|
+
RunEventsResponse,
|
|
26
|
+
RunsResponse,
|
|
27
|
+
WorkflowArtifact as WorkflowArtifactSchema,
|
|
28
|
+
WorkflowArtifactGraph as WorkflowArtifactGraphSchema,
|
|
29
|
+
WorkflowEvent as WorkflowEventSchema,
|
|
30
|
+
WorkflowGraph as WorkflowGraphSchema,
|
|
31
|
+
WorkflowGraphEdge as WorkflowGraphEdgeSchema,
|
|
32
|
+
WorkflowGraphNode as WorkflowGraphNodeSchema,
|
|
33
|
+
WorkflowGraphNodeMetadata as WorkflowGraphNodeMetadataSchema,
|
|
34
|
+
WorkflowGraphNodeSchemas as WorkflowGraphNodeSchemasSchema,
|
|
35
|
+
WorkflowGraphSchemas as WorkflowGraphSchemasSchema,
|
|
36
|
+
WorkflowHistoryEvent as WorkflowHistoryEventSchema,
|
|
37
|
+
WorkflowManifest,
|
|
38
|
+
WorkflowManifestEntry,
|
|
39
|
+
WorkflowRunEventRecord as WorkflowRunEventRecordSchema,
|
|
40
|
+
WorkflowRunRecord as WorkflowRunRecordSchema,
|
|
41
|
+
WorkflowRunStatus as WorkflowRunStatusSchema,
|
|
42
|
+
WorkflowsResponse,
|
|
43
|
+
decodeRunEventsResponse,
|
|
44
|
+
decodeRunsResponse,
|
|
45
|
+
decodeWorkflowsResponse,
|
|
46
|
+
decodeJsonSchema,
|
|
47
|
+
isWorkflowEvent
|
|
48
|
+
} from "./schemas.ts"
|
|
49
|
+
export type { JsonSchema as JsonSchemaDocument, ExecutionId as ExecutionIdValue } from "./schemas.ts"
|
|
50
|
+
export { deliverSignal, SignalDeliveryError } from "./signal.ts"
|
|
51
|
+
export { t } from "./schema.ts"
|
|
52
|
+
export { createWorkflowRuntime, engineLayer, executeWorkflow, makeEngineLayer, makeWorkflowEffect, run, WorkflowVersionConflictError } from "./runtime.ts"
|
|
53
|
+
export type { ExecuteWorkflowOptions, WorkflowRuntime, WorkflowRuntimeOptions } from "./runtime.ts"
|
|
54
|
+
export type {
|
|
55
|
+
FileWorkflowStoreOptions,
|
|
56
|
+
LoadedWorkflow,
|
|
57
|
+
RunWorkflowOptions,
|
|
58
|
+
WorkflowArtifact,
|
|
59
|
+
WorkflowRunResult,
|
|
60
|
+
WorkflowSdk,
|
|
61
|
+
WorkflowSdkOptions,
|
|
62
|
+
WorkflowStore,
|
|
63
|
+
WorkflowRepository,
|
|
64
|
+
WorkflowClient,
|
|
65
|
+
WorkflowExecutionHandle,
|
|
66
|
+
WorkflowExecutionStatus,
|
|
67
|
+
WorkflowHistoryEvent,
|
|
68
|
+
WorkflowHistoryRecord,
|
|
69
|
+
WorkflowListResult,
|
|
70
|
+
PendingSignal,
|
|
71
|
+
WorkflowRunEventRecord,
|
|
72
|
+
WorkflowRunRecord,
|
|
73
|
+
WorkflowRunStatus,
|
|
74
|
+
WorkflowRunStore,
|
|
75
|
+
SqliteWorkflowRepositoryOptions,
|
|
76
|
+
WorkflowArtifactGraph,
|
|
77
|
+
WorkflowGraph,
|
|
78
|
+
WorkflowGraphEdge,
|
|
79
|
+
WorkflowGraphNode,
|
|
80
|
+
WorkflowGraphNodeSchemas,
|
|
81
|
+
WorkflowGraphNodeMetadata,
|
|
82
|
+
WorkflowGraphSchemas,
|
|
83
|
+
WorkflowGraphNodeKind,
|
|
84
|
+
WorkflowGraphOptions,
|
|
85
|
+
DiscoverIntegrationsOptions,
|
|
86
|
+
DiscoverIntegrationsResult,
|
|
87
|
+
IntegrationKind,
|
|
88
|
+
IntegrationSearchResult
|
|
89
|
+
} from "./sdk/index.ts"
|
|
90
|
+
export {
|
|
91
|
+
Cancelled,
|
|
92
|
+
MissingWorkflowVersionError,
|
|
93
|
+
createFileWorkflowStore,
|
|
94
|
+
createMemoryWorkflowStore,
|
|
95
|
+
createWorkflowClient,
|
|
96
|
+
createSqliteWorkflowRepository,
|
|
97
|
+
seedSqliteWorkflowRepository,
|
|
98
|
+
loadWorkflowArtifact,
|
|
99
|
+
createWorkflowSdk,
|
|
100
|
+
parseJsonText,
|
|
101
|
+
toJsonText,
|
|
102
|
+
sampleValueForJsonSchema,
|
|
103
|
+
sampleValueForSchema,
|
|
104
|
+
workflowArtifactToGraph,
|
|
105
|
+
workflowToGraph,
|
|
106
|
+
discover
|
|
107
|
+
} from "./sdk/index.ts"
|
|
108
|
+
export { createTestRuntime } from "./testing/index.ts"
|
|
109
|
+
export type { CompensationRecorder, TestRuntime, TestRuntimeOptions } from "./testing/index.ts"
|
package/src/replay.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WorkflowHistoryEvent } from "./schemas.ts"
|
|
2
|
+
|
|
3
|
+
// Events emitted from workflow-position code (as opposed to inside an
|
|
4
|
+
// activity) fire again every time the durable engine replays an execution —
|
|
5
|
+
// e.g. when a suspended run resumes after a signal, possibly in a fresh
|
|
6
|
+
// process. History sinks use this identity to record each such event once.
|
|
7
|
+
// Step, code, and compensation events are emitted inside activities or
|
|
8
|
+
// finalizers, never replayed, and legitimately repeat across retry attempts,
|
|
9
|
+
// so they have no dedupe identity.
|
|
10
|
+
export const replayDedupeKey = (event: WorkflowHistoryEvent): string | undefined => {
|
|
11
|
+
switch (event.type) {
|
|
12
|
+
case "workflow.started":
|
|
13
|
+
case "workflow.completed":
|
|
14
|
+
case "workflow.failed":
|
|
15
|
+
case "cancellation.received":
|
|
16
|
+
return event.type
|
|
17
|
+
case "sleep.started":
|
|
18
|
+
case "sleep.completed":
|
|
19
|
+
case "signal.waiting":
|
|
20
|
+
case "signal.received":
|
|
21
|
+
case "signal.timeout":
|
|
22
|
+
case "all.started":
|
|
23
|
+
case "all.completed":
|
|
24
|
+
case "all.failed":
|
|
25
|
+
return `${event.type}:${event.activityName}`
|
|
26
|
+
default:
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
4
|
+
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
|
5
|
+
import { Effect, Exit, Layer, ManagedRuntime, Schema } from "effect"
|
|
6
|
+
import { ClusterWorkflowEngine, SingleRunner } from "effect/unstable/cluster"
|
|
7
|
+
import { SqlClient } from "effect/unstable/sql"
|
|
8
|
+
import { DurableDeferred, WorkflowEngine } from "effect/unstable/workflow"
|
|
9
|
+
import { currentSecretResolver, removeExecutionSecretResolver, setExecutionSecretResolver } from "./core.ts"
|
|
10
|
+
import type { DefinedWorkflow, SecretResolver } from "./core.ts"
|
|
11
|
+
import {
|
|
12
|
+
currentWorkflowEventSink,
|
|
13
|
+
emitWorkflowEvent,
|
|
14
|
+
removeExecutionEventSink,
|
|
15
|
+
setExecutionEventSink
|
|
16
|
+
} from "./events.ts"
|
|
17
|
+
import type { WorkflowEventSink } from "./events.ts"
|
|
18
|
+
|
|
19
|
+
export interface ExecuteWorkflowOptions {
|
|
20
|
+
readonly onEvent?: WorkflowEventSink
|
|
21
|
+
readonly engineDatabasePath?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkflowRuntimeOptions {
|
|
25
|
+
readonly backend: "memory" | "sqlite"
|
|
26
|
+
readonly databasePath?: string
|
|
27
|
+
/** Resolves SecretRef inputs to their values at step execution time.
|
|
28
|
+
* Only the reference string is ever persisted. */
|
|
29
|
+
readonly secrets?: SecretResolver
|
|
30
|
+
readonly sqliteBusyTimeoutMs?: number
|
|
31
|
+
/** How often the engine polls storage for due timers and undelivered
|
|
32
|
+
* messages. Durable timers (signal timeouts, long sleeps) can fire up to
|
|
33
|
+
* one interval late. Defaults to 250ms. */
|
|
34
|
+
readonly timerPollIntervalMs?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkflowRuntime {
|
|
38
|
+
readonly backend: "memory" | "sqlite"
|
|
39
|
+
readonly databasePath?: string
|
|
40
|
+
readonly secrets?: SecretResolver
|
|
41
|
+
register(workflows: ReadonlyArray<any>): void
|
|
42
|
+
getWorkflow(name: string, version: number): DefinedWorkflow | undefined
|
|
43
|
+
getLatestWorkflow(name: string): DefinedWorkflow | undefined
|
|
44
|
+
listWorkflows(name?: string): ReadonlyArray<DefinedWorkflow>
|
|
45
|
+
execute(options: {
|
|
46
|
+
readonly workflow: DefinedWorkflow
|
|
47
|
+
readonly payload: unknown
|
|
48
|
+
readonly executionId: string
|
|
49
|
+
readonly onEvent?: WorkflowEventSink
|
|
50
|
+
}): Promise<unknown>
|
|
51
|
+
deliverSignal(options: {
|
|
52
|
+
readonly workflow: DefinedWorkflow
|
|
53
|
+
readonly executionId: string
|
|
54
|
+
readonly deferredName: string
|
|
55
|
+
readonly payload: unknown
|
|
56
|
+
readonly onEvent?: WorkflowEventSink
|
|
57
|
+
}): Promise<void>
|
|
58
|
+
interrupt(options: {
|
|
59
|
+
readonly workflow: DefinedWorkflow
|
|
60
|
+
readonly executionId: string
|
|
61
|
+
}): Promise<void>
|
|
62
|
+
/** Wake a suspended execution so it replays to its suspension point.
|
|
63
|
+
* No-op unless the run is recorded as suspended. */
|
|
64
|
+
resume(options: {
|
|
65
|
+
readonly workflow: DefinedWorkflow
|
|
66
|
+
readonly executionId: string
|
|
67
|
+
}): Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class WorkflowVersionConflictError extends Error {
|
|
71
|
+
readonly _tag = "WorkflowVersionConflictError"
|
|
72
|
+
|
|
73
|
+
constructor(options: { readonly name: string; readonly version: number }) {
|
|
74
|
+
super(
|
|
75
|
+
`Workflow ${options.name}@v${options.version} is already registered with different source; register a new version instead`
|
|
76
|
+
)
|
|
77
|
+
this.name = "WorkflowVersionConflictError"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const defaultEngineDatabasePath = () => path.join(process.cwd(), ".wf", "engine.sqlite")
|
|
82
|
+
|
|
83
|
+
// All durable-execution plumbing lives here so authored workflows never touch
|
|
84
|
+
// the cluster engine, the runner, or the backing store.
|
|
85
|
+
export const makeEngineLayer = (options: {
|
|
86
|
+
readonly databasePath?: string
|
|
87
|
+
readonly sqliteBusyTimeoutMs?: number
|
|
88
|
+
readonly timerPollIntervalMs?: number
|
|
89
|
+
} = {}) => {
|
|
90
|
+
const databasePath = path.resolve(options.databasePath ?? defaultEngineDatabasePath())
|
|
91
|
+
const sqliteBusyTimeoutMs = Math.max(0, Math.trunc(options.sqliteBusyTimeoutMs ?? 5000))
|
|
92
|
+
// The cluster default is 10 seconds, which delays every durable timer
|
|
93
|
+
// (signal timeout, long sleep) by up to that long on a single-node engine.
|
|
94
|
+
const timerPollIntervalMs = Math.max(10, Math.trunc(options.timerPollIntervalMs ?? 250))
|
|
95
|
+
mkdirSync(path.dirname(databasePath), { recursive: true })
|
|
96
|
+
const sqliteLayer = SqliteClient.layer({ filename: databasePath })
|
|
97
|
+
const configuredSqliteLayer = Layer.effectDiscard(Effect.gen(function* () {
|
|
98
|
+
const sql = yield* SqlClient.SqlClient
|
|
99
|
+
yield* sql.unsafe(`PRAGMA busy_timeout = ${sqliteBusyTimeoutMs}`)
|
|
100
|
+
})).pipe(Layer.provideMerge(sqliteLayer))
|
|
101
|
+
|
|
102
|
+
return ClusterWorkflowEngine.layer.pipe(
|
|
103
|
+
Layer.provideMerge(SingleRunner.layer({
|
|
104
|
+
shardingConfig: { entityMessagePollInterval: timerPollIntervalMs }
|
|
105
|
+
})),
|
|
106
|
+
Layer.provide(configuredSqliteLayer)
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const engineLayer = makeEngineLayer()
|
|
111
|
+
|
|
112
|
+
export const createWorkflowRuntime = (options: WorkflowRuntimeOptions): WorkflowRuntime => {
|
|
113
|
+
const workflows = new Map<string, DefinedWorkflow>()
|
|
114
|
+
const databasePath = options.databasePath
|
|
115
|
+
|
|
116
|
+
const env = () => {
|
|
117
|
+
const workflowLayers = Array.from(workflows.values()).map((workflow) => workflow.layer)
|
|
118
|
+
const base =
|
|
119
|
+
options.backend === "sqlite"
|
|
120
|
+
? makeEngineLayer({
|
|
121
|
+
...(databasePath === undefined ? {} : { databasePath }),
|
|
122
|
+
...(options.sqliteBusyTimeoutMs === undefined ? {} : { sqliteBusyTimeoutMs: options.sqliteBusyTimeoutMs }),
|
|
123
|
+
...(options.timerPollIntervalMs === undefined ? {} : { timerPollIntervalMs: options.timerPollIntervalMs })
|
|
124
|
+
})
|
|
125
|
+
: WorkflowEngine.layerMemory
|
|
126
|
+
return workflowLayers.reduce((layer, workflowLayer) => Layer.provideMerge(workflowLayer, layer), base)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ONE engine per runtime. Building a fresh cluster node per call (execute,
|
|
130
|
+
// signal, ...) puts several SingleRunner nodes on the same store, and they
|
|
131
|
+
// fight over shard ownership — message processing becomes arbitrarily late.
|
|
132
|
+
// Rebuilt only when the registered workflow set changes.
|
|
133
|
+
let managed: { readonly signature: string; readonly runtime: ManagedRuntime.ManagedRuntime<any, unknown> } | undefined
|
|
134
|
+
|
|
135
|
+
const getManagedRuntime = () => {
|
|
136
|
+
const signature = Array.from(workflows.keys()).sort().join(",")
|
|
137
|
+
if (managed === undefined || managed.signature !== signature) {
|
|
138
|
+
if (managed !== undefined) {
|
|
139
|
+
void managed.runtime.dispose()
|
|
140
|
+
}
|
|
141
|
+
managed = { signature, runtime: ManagedRuntime.make(env()) }
|
|
142
|
+
}
|
|
143
|
+
return managed.runtime
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const runEffect = <A>(effect: Effect.Effect<A, unknown, any>, onEvent?: WorkflowEventSink) =>
|
|
147
|
+
getManagedRuntime().runPromise(
|
|
148
|
+
effect.pipe(
|
|
149
|
+
Effect.provideService(currentWorkflowEventSink, onEvent),
|
|
150
|
+
Effect.provideService(currentSecretResolver, options.secrets)
|
|
151
|
+
) as Effect.Effect<A, unknown, never>
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
backend: options.backend,
|
|
156
|
+
...(databasePath === undefined ? {} : { databasePath }),
|
|
157
|
+
...(options.secrets === undefined ? {} : { secrets: options.secrets }),
|
|
158
|
+
|
|
159
|
+
register(registered) {
|
|
160
|
+
for (const workflow of registered) {
|
|
161
|
+
const key = `${workflow.name}@${workflow.version}`
|
|
162
|
+
const existing = workflows.get(key)
|
|
163
|
+
if (existing !== undefined && existing.sourceHash !== workflow.sourceHash) {
|
|
164
|
+
throw new WorkflowVersionConflictError({
|
|
165
|
+
name: workflow.name,
|
|
166
|
+
version: workflow.version
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
workflows.set(key, workflow)
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
getWorkflow(name, version) {
|
|
174
|
+
return workflows.get(`${name}@${version}`)
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
getLatestWorkflow(name) {
|
|
178
|
+
return Array.from(workflows.values())
|
|
179
|
+
.filter((workflow) => workflow.name === name)
|
|
180
|
+
.sort((left, right) => right.version - left.version)[0]
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
listWorkflows(name) {
|
|
184
|
+
return Array.from(workflows.values())
|
|
185
|
+
.filter((workflow) => name === undefined || workflow.name === name)
|
|
186
|
+
.sort((left, right) => left.name.localeCompare(right.name) || left.version - right.version)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
execute({ workflow, payload, executionId, onEvent }) {
|
|
190
|
+
const workflowName = String(workflow.workflow._tag ?? workflow.engineName)
|
|
191
|
+
if (onEvent !== undefined) {
|
|
192
|
+
setExecutionEventSink(executionId, onEvent)
|
|
193
|
+
}
|
|
194
|
+
if (options.secrets !== undefined) {
|
|
195
|
+
setExecutionSecretResolver(executionId, options.secrets)
|
|
196
|
+
}
|
|
197
|
+
const effect = Effect.gen(function* () {
|
|
198
|
+
const engine = yield* WorkflowEngine.WorkflowEngine
|
|
199
|
+
yield* emitWorkflowEvent({ type: "workflow.started", workflowName, payload })
|
|
200
|
+
const result = yield* engine.execute(workflow.workflow, {
|
|
201
|
+
executionId,
|
|
202
|
+
payload: payload as any
|
|
203
|
+
}).pipe(
|
|
204
|
+
Effect.tap((result: unknown) =>
|
|
205
|
+
emitWorkflowEvent({ type: "workflow.completed", workflowName, result })
|
|
206
|
+
),
|
|
207
|
+
Effect.tapError((error: unknown) =>
|
|
208
|
+
emitWorkflowEvent({ type: "workflow.failed", workflowName, error })
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
return result
|
|
212
|
+
})
|
|
213
|
+
return runEffect(effect, onEvent).finally(() => {
|
|
214
|
+
removeExecutionEventSink(executionId)
|
|
215
|
+
removeExecutionSecretResolver(executionId)
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
deliverSignal({ workflow, executionId, deferredName, payload, onEvent }) {
|
|
220
|
+
if (onEvent !== undefined) {
|
|
221
|
+
setExecutionEventSink(executionId, onEvent)
|
|
222
|
+
}
|
|
223
|
+
// The resumed replay may execute steps in THIS process (see the resume
|
|
224
|
+
// note below), so it needs the secret resolver just like execute().
|
|
225
|
+
if (options.secrets !== undefined) {
|
|
226
|
+
setExecutionSecretResolver(executionId, options.secrets)
|
|
227
|
+
}
|
|
228
|
+
const effect = Effect.gen(function* () {
|
|
229
|
+
const engine = yield* WorkflowEngine.WorkflowEngine
|
|
230
|
+
const deferred = DurableDeferred.make(deferredName, { success: Schema.Unknown })
|
|
231
|
+
yield* engine.deferredDone(deferred, {
|
|
232
|
+
workflowName: workflow.workflow._tag,
|
|
233
|
+
executionId,
|
|
234
|
+
deferredName,
|
|
235
|
+
exit: Exit.succeed(payload)
|
|
236
|
+
})
|
|
237
|
+
// deferredDone only resumes a run whose Suspended reply is already
|
|
238
|
+
// persisted. A delivery racing the suspension write would otherwise
|
|
239
|
+
// sit unnoticed until another wake-up, so nudge resume a few times
|
|
240
|
+
// (resume is a no-op unless the run is recorded as suspended).
|
|
241
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
242
|
+
yield* Effect.sleep("100 millis")
|
|
243
|
+
yield* engine.resume(workflow.workflow, executionId)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
// The resumed replay may run inside THIS call's engine environment, so
|
|
247
|
+
// it needs the same event sink as the original execute to record
|
|
248
|
+
// history (compensations, cancellation, signal receipt).
|
|
249
|
+
return runEffect(effect, onEvent)
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
interrupt({ workflow, executionId }) {
|
|
253
|
+
const effect = Effect.gen(function* () {
|
|
254
|
+
const engine = yield* WorkflowEngine.WorkflowEngine
|
|
255
|
+
yield* engine.interrupt(workflow.workflow, executionId)
|
|
256
|
+
})
|
|
257
|
+
return runEffect(effect)
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
resume({ workflow, executionId }) {
|
|
261
|
+
const effect = Effect.gen(function* () {
|
|
262
|
+
const engine = yield* WorkflowEngine.WorkflowEngine
|
|
263
|
+
yield* engine.resume(workflow.workflow, executionId)
|
|
264
|
+
})
|
|
265
|
+
return runEffect(effect)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export const makeWorkflowEffect = (
|
|
271
|
+
wf: DefinedWorkflow,
|
|
272
|
+
payload: unknown,
|
|
273
|
+
options: ExecuteWorkflowOptions = {}
|
|
274
|
+
) => {
|
|
275
|
+
const env = wf.layer.pipe(
|
|
276
|
+
Layer.provideMerge(makeEngineLayer(
|
|
277
|
+
options.engineDatabasePath === undefined ? {} : { databasePath: options.engineDatabasePath }
|
|
278
|
+
))
|
|
279
|
+
)
|
|
280
|
+
const workflowName = String(wf.workflow._tag ?? wf.engineName ?? "Workflow")
|
|
281
|
+
const execution = Effect.gen(function* () {
|
|
282
|
+
yield* emitWorkflowEvent({ type: "workflow.started", workflowName, payload })
|
|
283
|
+
const result = yield* wf.workflow.execute(payload).pipe(
|
|
284
|
+
Effect.tap((result: unknown) =>
|
|
285
|
+
emitWorkflowEvent({ type: "workflow.completed", workflowName, result })
|
|
286
|
+
),
|
|
287
|
+
Effect.tapError((error: unknown) =>
|
|
288
|
+
emitWorkflowEvent({ type: "workflow.failed", workflowName, error })
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
return result
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return execution.pipe(
|
|
295
|
+
Effect.provide(env),
|
|
296
|
+
Effect.provideService(currentWorkflowEventSink, options.onEvent)
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const executeWorkflow = (
|
|
301
|
+
wf: DefinedWorkflow,
|
|
302
|
+
payload: unknown,
|
|
303
|
+
options: ExecuteWorkflowOptions = {}
|
|
304
|
+
) =>
|
|
305
|
+
Effect.runPromise(
|
|
306
|
+
makeWorkflowEffect(wf, payload, options) as Effect.Effect<unknown, unknown, never>
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
// Execute a workflow to completion as a standalone program.
|
|
310
|
+
export const run = (wf: DefinedWorkflow, payload: unknown) => {
|
|
311
|
+
return (makeWorkflowEffect(wf, payload) as Effect.Effect<unknown, unknown, never>).pipe(
|
|
312
|
+
NodeRuntime.runMain
|
|
313
|
+
)
|
|
314
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
interface SchemaVocabulary {
|
|
4
|
+
readonly string: typeof Schema.String
|
|
5
|
+
readonly number: typeof Schema.Number
|
|
6
|
+
readonly boolean: typeof Schema.Boolean
|
|
7
|
+
readonly void: typeof Schema.Void
|
|
8
|
+
readonly date: typeof Schema.Date
|
|
9
|
+
readonly struct: typeof Schema.Struct
|
|
10
|
+
readonly array: <S extends Schema.Constraint>(schema: S) => Schema.$Array<S>
|
|
11
|
+
readonly literal: typeof Schema.Literal
|
|
12
|
+
readonly taggedStruct: typeof Schema.TaggedStruct
|
|
13
|
+
readonly optional: <S extends Schema.Constraint>(schema: S) => Schema.optional<S>
|
|
14
|
+
readonly union: typeof Schema.Union
|
|
15
|
+
readonly unknown: typeof Schema.Unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// `t` is the LLM-facing schema vocabulary. We re-export a small, lowercase
|
|
19
|
+
// subset of Effect's `Schema` so authored workflows never import `effect`
|
|
20
|
+
// directly. Add primitives here as workflows need them — keep it small.
|
|
21
|
+
export const t: SchemaVocabulary = {
|
|
22
|
+
string: Schema.String,
|
|
23
|
+
number: Schema.Number,
|
|
24
|
+
boolean: Schema.Boolean,
|
|
25
|
+
void: Schema.Void,
|
|
26
|
+
date: Schema.Date,
|
|
27
|
+
struct: Schema.Struct,
|
|
28
|
+
array: Schema.Array,
|
|
29
|
+
literal: Schema.Literal,
|
|
30
|
+
taggedStruct: Schema.TaggedStruct,
|
|
31
|
+
optional: Schema.optional,
|
|
32
|
+
union: Schema.Union,
|
|
33
|
+
unknown: Schema.Unknown
|
|
34
|
+
} as const
|