@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type * as Duration from "effect/Duration"
|
|
2
|
+
import type {
|
|
3
|
+
DefinedWorkflow,
|
|
4
|
+
InMemoryDeterminismState,
|
|
5
|
+
Step,
|
|
6
|
+
StepContext,
|
|
7
|
+
TerminalFailure
|
|
8
|
+
} from "../core.ts"
|
|
9
|
+
import { createInMemoryDeterminismState } from "../core.ts"
|
|
10
|
+
import type {
|
|
11
|
+
WorkflowExecutionHandle,
|
|
12
|
+
WorkflowExecutionStatus,
|
|
13
|
+
WorkflowHistoryEvent,
|
|
14
|
+
WorkflowHistoryRecord,
|
|
15
|
+
WorkflowResult
|
|
16
|
+
} from "../sdk/index.ts"
|
|
17
|
+
import { Cancelled } from "../sdk/index.ts"
|
|
18
|
+
import { cancelSignalWaits, deliverSignal } from "../signal.ts"
|
|
19
|
+
import { ExecutionId } from "../schemas.ts"
|
|
20
|
+
|
|
21
|
+
export interface TestRuntimeOptions {
|
|
22
|
+
readonly timeSkipping?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CompensationRecorder {
|
|
26
|
+
readonly calls: Array<{ readonly step: string; readonly result: unknown }>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TestRuntime {
|
|
30
|
+
mockStep<I, O, E>(
|
|
31
|
+
step: Step<I, O, E>,
|
|
32
|
+
impl: (input: I, step: StepContext<E>) => Promise<O | TerminalFailure<E>>
|
|
33
|
+
): void
|
|
34
|
+
failStepOnce<I, O, E>(step: Step<I, O, E>): void
|
|
35
|
+
recordCompensations(): CompensationRecorder
|
|
36
|
+
start<I, O, E>(
|
|
37
|
+
workflow: DefinedWorkflow<I, O, E>,
|
|
38
|
+
payload: I,
|
|
39
|
+
opts?: { readonly idempotencyKey?: string; readonly actor?: string }
|
|
40
|
+
): Promise<WorkflowExecutionHandle>
|
|
41
|
+
replay<I, O, E>(
|
|
42
|
+
executionId: string,
|
|
43
|
+
workflow: DefinedWorkflow<I, O, E>,
|
|
44
|
+
payload: I
|
|
45
|
+
): Promise<WorkflowExecutionHandle>
|
|
46
|
+
sendSignal(executionId: string, name: string, payload: unknown): Promise<void>
|
|
47
|
+
result(executionId: string): Promise<WorkflowResult>
|
|
48
|
+
status(executionId: string): Promise<WorkflowExecutionStatus>
|
|
49
|
+
history(executionId: string): Promise<ReadonlyArray<WorkflowHistoryRecord>>
|
|
50
|
+
cancel(executionId: string, opts?: { readonly compensate?: boolean; readonly actor?: string }): Promise<void>
|
|
51
|
+
advanceTime(duration: Duration.Input): Promise<void>
|
|
52
|
+
/** Register a secret value so SecretRef inputs resolve inside step execute. */
|
|
53
|
+
setSecret(name: string, value: string): void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ExecutionRecord {
|
|
57
|
+
readonly executionId: string
|
|
58
|
+
readonly workflow: DefinedWorkflow
|
|
59
|
+
readonly payload: unknown
|
|
60
|
+
readonly determinism: InMemoryDeterminismState
|
|
61
|
+
status: WorkflowExecutionStatus
|
|
62
|
+
result?: WorkflowResult
|
|
63
|
+
readonly startedAt: string
|
|
64
|
+
finishedAt?: string
|
|
65
|
+
readonly history: WorkflowHistoryRecord[]
|
|
66
|
+
readonly resultPromise: Promise<WorkflowResult>
|
|
67
|
+
readonly resolveResult: (result: WorkflowResult) => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface VirtualTimer {
|
|
71
|
+
readonly due: number
|
|
72
|
+
readonly resolve: () => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nowIso = () => new Date().toISOString()
|
|
76
|
+
const executionId = () => crypto.randomUUID()
|
|
77
|
+
|
|
78
|
+
const parseDurationMs = (duration: Duration.Input): number => {
|
|
79
|
+
if (typeof duration === "number") {
|
|
80
|
+
return duration
|
|
81
|
+
}
|
|
82
|
+
const raw = String(duration).trim()
|
|
83
|
+
const match = /^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)?$/.exec(raw)
|
|
84
|
+
if (match === null) {
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
const value = Number(match[1])
|
|
88
|
+
const unit = (match[2] ?? "millis").toLowerCase()
|
|
89
|
+
if (unit.startsWith("ms") || unit.startsWith("milli")) return value
|
|
90
|
+
if (unit.startsWith("sec")) return value * 1_000
|
|
91
|
+
if (unit.startsWith("min")) return value * 60_000
|
|
92
|
+
if (unit.startsWith("hour")) return value * 60 * 60_000
|
|
93
|
+
if (unit.startsWith("day")) return value * 24 * 60 * 60_000
|
|
94
|
+
return value
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const statusFromEvent = (event: WorkflowHistoryEvent): WorkflowExecutionStatus | undefined => {
|
|
98
|
+
switch (event.type) {
|
|
99
|
+
case "sleep.started":
|
|
100
|
+
case "signal.waiting":
|
|
101
|
+
return "suspended"
|
|
102
|
+
case "sleep.completed":
|
|
103
|
+
case "signal.received":
|
|
104
|
+
case "signal.timeout":
|
|
105
|
+
case "step.started":
|
|
106
|
+
case "step.completed":
|
|
107
|
+
return "running"
|
|
108
|
+
default:
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const createTestRuntime = (options: TestRuntimeOptions = {}): TestRuntime => {
|
|
114
|
+
const timeSkipping = options.timeSkipping ?? true
|
|
115
|
+
const executions = new Map<string, ExecutionRecord>()
|
|
116
|
+
const idempotencyKeys = new Map<string, string>()
|
|
117
|
+
const stepMocks = new Map<Step<any, any, any>, Step<any, any, any>["execute"]>()
|
|
118
|
+
const failOnce = new Set<Step<any, any, any>>()
|
|
119
|
+
const recorders: CompensationRecorder[] = []
|
|
120
|
+
const timers: VirtualTimer[] = []
|
|
121
|
+
const secrets = new Map<string, string>()
|
|
122
|
+
let virtualNow = 0
|
|
123
|
+
|
|
124
|
+
const secretResolver = {
|
|
125
|
+
resolve: (name: string) => {
|
|
126
|
+
const value = secrets.get(name)
|
|
127
|
+
if (value === undefined) {
|
|
128
|
+
throw new Error(`Unknown test secret: ${name} (register it with rt.setSecret)`)
|
|
129
|
+
}
|
|
130
|
+
return value
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const appendHistory = (record: ExecutionRecord, event: WorkflowHistoryEvent) => {
|
|
135
|
+
record.history.push({
|
|
136
|
+
sequence: record.history.length + 1,
|
|
137
|
+
createdAt: nowIso(),
|
|
138
|
+
event
|
|
139
|
+
})
|
|
140
|
+
const nextStatus = statusFromEvent(event)
|
|
141
|
+
if (nextStatus !== undefined && record.status !== "failed") {
|
|
142
|
+
record.status = nextStatus
|
|
143
|
+
}
|
|
144
|
+
if (event.type === "compensation.started") {
|
|
145
|
+
for (const recorder of recorders) {
|
|
146
|
+
recorder.calls.push({ step: event.stepName, result: event.result })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const requireExecution = (id: string): ExecutionRecord => {
|
|
152
|
+
const execution = executions.get(id)
|
|
153
|
+
if (execution === undefined) {
|
|
154
|
+
throw new Error(`Unknown workflow execution: ${id}`)
|
|
155
|
+
}
|
|
156
|
+
return execution
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const makeDelay = (duration: Duration.Input): Promise<void> => {
|
|
160
|
+
if (timeSkipping) {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
162
|
+
}
|
|
163
|
+
const due = virtualNow + parseDurationMs(duration)
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
timers.push({ due, resolve })
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const buildStepExecutors = () => {
|
|
170
|
+
const executors = new Map<Step<any, any, any>, Step<any, any, any>["execute"]>()
|
|
171
|
+
const steps = new Set([...stepMocks.keys(), ...failOnce])
|
|
172
|
+
for (const step of steps) {
|
|
173
|
+
executors.set(step, async (input: unknown, context: StepContext<unknown>) => {
|
|
174
|
+
if (failOnce.has(step)) {
|
|
175
|
+
failOnce.delete(step)
|
|
176
|
+
throw new Error(`Injected failure for step ${step.name}`)
|
|
177
|
+
}
|
|
178
|
+
const mock = stepMocks.get(step)
|
|
179
|
+
return mock === undefined
|
|
180
|
+
? step.execute(input, context as never)
|
|
181
|
+
: mock(input, context as never)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
return executors
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const launch = (
|
|
188
|
+
workflow: DefinedWorkflow,
|
|
189
|
+
payload: unknown,
|
|
190
|
+
record: ExecutionRecord
|
|
191
|
+
) => {
|
|
192
|
+
void workflow.executeInMemory(payload as never, {
|
|
193
|
+
executionId: record.executionId,
|
|
194
|
+
determinism: record.determinism,
|
|
195
|
+
stepExecutors: buildStepExecutors(),
|
|
196
|
+
sleep: ({ duration }) => makeDelay(duration),
|
|
197
|
+
signalTimeout: ({ duration }) => makeDelay(duration),
|
|
198
|
+
secrets: secretResolver,
|
|
199
|
+
onEvent: (event) => {
|
|
200
|
+
appendHistory(record, event as WorkflowHistoryEvent)
|
|
201
|
+
}
|
|
202
|
+
}).then(
|
|
203
|
+
(value) => {
|
|
204
|
+
record.status = "completed"
|
|
205
|
+
record.finishedAt = nowIso()
|
|
206
|
+
record.result = { type: "completed", value }
|
|
207
|
+
record.resolveResult(record.result)
|
|
208
|
+
},
|
|
209
|
+
(error) => {
|
|
210
|
+
record.status = "failed"
|
|
211
|
+
record.finishedAt = nowIso()
|
|
212
|
+
record.result = { type: "failed", error }
|
|
213
|
+
record.resolveResult(record.result)
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const createExecution = (
|
|
219
|
+
workflow: DefinedWorkflow,
|
|
220
|
+
payload: unknown,
|
|
221
|
+
opts: { readonly id?: string; readonly determinism?: InMemoryDeterminismState; readonly actor?: string } = {}
|
|
222
|
+
): ExecutionRecord => {
|
|
223
|
+
let resolveResult!: (result: WorkflowResult) => void
|
|
224
|
+
const resultPromise = new Promise<WorkflowResult>((resolve) => {
|
|
225
|
+
resolveResult = resolve
|
|
226
|
+
})
|
|
227
|
+
const id = opts.id ?? executionId()
|
|
228
|
+
const record: ExecutionRecord = {
|
|
229
|
+
executionId: id,
|
|
230
|
+
workflow,
|
|
231
|
+
payload,
|
|
232
|
+
determinism: opts.determinism ?? createInMemoryDeterminismState(),
|
|
233
|
+
status: "running",
|
|
234
|
+
startedAt: nowIso(),
|
|
235
|
+
history: [],
|
|
236
|
+
resultPromise,
|
|
237
|
+
resolveResult
|
|
238
|
+
}
|
|
239
|
+
executions.set(id, record)
|
|
240
|
+
appendHistory(record, {
|
|
241
|
+
type: "execution.started",
|
|
242
|
+
executionId: ExecutionId.make(id),
|
|
243
|
+
workflowName: workflow.name,
|
|
244
|
+
version: workflow.version,
|
|
245
|
+
payload,
|
|
246
|
+
...(opts.actor === undefined ? {} : { actor: opts.actor })
|
|
247
|
+
})
|
|
248
|
+
launch(workflow, payload, record)
|
|
249
|
+
return record
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
mockStep(step, impl) {
|
|
254
|
+
stepMocks.set(step, impl as Step<any, any, any>["execute"])
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
failStepOnce(step) {
|
|
258
|
+
failOnce.add(step as Step<any, any, any>)
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
recordCompensations() {
|
|
262
|
+
const recorder: CompensationRecorder = { calls: [] }
|
|
263
|
+
recorders.push(recorder)
|
|
264
|
+
return recorder
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async start(workflow, payload, opts = {}) {
|
|
268
|
+
const workflowKey = `${workflow.name}@v${workflow.version}`
|
|
269
|
+
if (opts.idempotencyKey !== undefined) {
|
|
270
|
+
const existingId = idempotencyKeys.get(`${workflowKey}:${opts.idempotencyKey}`)
|
|
271
|
+
if (existingId !== undefined) {
|
|
272
|
+
return { executionId: existingId, version: workflow.version }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const record = createExecution(workflow, payload, opts.actor === undefined ? {} : { actor: opts.actor })
|
|
276
|
+
if (opts.idempotencyKey !== undefined) {
|
|
277
|
+
idempotencyKeys.set(`${workflowKey}:${opts.idempotencyKey}`, record.executionId)
|
|
278
|
+
}
|
|
279
|
+
return { executionId: record.executionId, version: workflow.version }
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async replay(id, workflow, payload) {
|
|
283
|
+
const previous = requireExecution(id)
|
|
284
|
+
const record = createExecution(workflow, payload, {
|
|
285
|
+
id,
|
|
286
|
+
determinism: previous.determinism
|
|
287
|
+
})
|
|
288
|
+
return { executionId: record.executionId, version: workflow.version }
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
sendSignal(executionId, name, payload) {
|
|
292
|
+
return deliverSignal(executionId, name, payload)
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
result(executionId) {
|
|
296
|
+
return requireExecution(executionId).resultPromise
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async status(executionId) {
|
|
300
|
+
return requireExecution(executionId).status
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async history(executionId) {
|
|
304
|
+
return requireExecution(executionId).history
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
async cancel(executionId, opts = {}) {
|
|
308
|
+
const record = requireExecution(executionId)
|
|
309
|
+
const compensate = opts.compensate ?? true
|
|
310
|
+
appendHistory(record, {
|
|
311
|
+
type: "execution.cancelled",
|
|
312
|
+
executionId: ExecutionId.make(executionId),
|
|
313
|
+
compensate,
|
|
314
|
+
...(opts.actor === undefined ? {} : { actor: opts.actor })
|
|
315
|
+
})
|
|
316
|
+
if (compensate) {
|
|
317
|
+
record.status = "compensating"
|
|
318
|
+
}
|
|
319
|
+
cancelSignalWaits(executionId, new Cancelled({ compensate }))
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
setSecret(name, value) {
|
|
323
|
+
secrets.set(name, value)
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async advanceTime(duration) {
|
|
327
|
+
virtualNow += parseDurationMs(duration)
|
|
328
|
+
timers.sort((left, right) => left.due - right.due)
|
|
329
|
+
for (let index = 0; index < timers.length;) {
|
|
330
|
+
const timer = timers[index]!
|
|
331
|
+
if (timer.due > virtualNow) {
|
|
332
|
+
index++
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
timers.splice(index, 1)
|
|
336
|
+
timer.resolve()
|
|
337
|
+
}
|
|
338
|
+
await Promise.resolve()
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|