@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/authoring.d.ts +7 -0
  4. package/dist/authoring.d.ts.map +1 -0
  5. package/dist/authoring.js +8 -0
  6. package/dist/authoring.js.map +1 -0
  7. package/dist/cli/integrations.d.ts +3 -0
  8. package/dist/cli/integrations.d.ts.map +1 -0
  9. package/dist/cli/integrations.js +40 -0
  10. package/dist/cli/integrations.js.map +1 -0
  11. package/dist/cli/main.d.ts +3 -0
  12. package/dist/cli/main.d.ts.map +1 -0
  13. package/dist/cli/main.js +591 -0
  14. package/dist/cli/main.js.map +1 -0
  15. package/dist/core.d.ts +198 -0
  16. package/dist/core.d.ts.map +1 -0
  17. package/dist/core.js +1061 -0
  18. package/dist/core.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +13 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/events.d.ts +154 -0
  24. package/dist/events.d.ts.map +1 -0
  25. package/dist/events.js +24 -0
  26. package/dist/events.js.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +11 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/replay.d.ts +3 -0
  32. package/dist/replay.d.ts.map +1 -0
  33. package/dist/replay.js +28 -0
  34. package/dist/replay.js.map +1 -0
  35. package/dist/runtime.d.ts +70 -0
  36. package/dist/runtime.d.ts.map +1 -0
  37. package/dist/runtime.js +183 -0
  38. package/dist/runtime.js.map +1 -0
  39. package/dist/schema.d.ts +18 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +19 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/schemas.d.ts +1198 -0
  44. package/dist/schemas.d.ts.map +1 -0
  45. package/dist/schemas.js +383 -0
  46. package/dist/schemas.js.map +1 -0
  47. package/dist/sdk/artifact.d.ts +41 -0
  48. package/dist/sdk/artifact.d.ts.map +1 -0
  49. package/dist/sdk/artifact.js +94 -0
  50. package/dist/sdk/artifact.js.map +1 -0
  51. package/dist/sdk/graph.d.ts +15 -0
  52. package/dist/sdk/graph.d.ts.map +1 -0
  53. package/dist/sdk/graph.js +388 -0
  54. package/dist/sdk/graph.js.map +1 -0
  55. package/dist/sdk/index.d.ts +14 -0
  56. package/dist/sdk/index.d.ts.map +1 -0
  57. package/dist/sdk/index.js +8 -0
  58. package/dist/sdk/index.js.map +1 -0
  59. package/dist/sdk/integrations.d.ts +18 -0
  60. package/dist/sdk/integrations.d.ts.map +1 -0
  61. package/dist/sdk/integrations.js +20 -0
  62. package/dist/sdk/integrations.js.map +1 -0
  63. package/dist/sdk/json.d.ts +4 -0
  64. package/dist/sdk/json.d.ts.map +1 -0
  65. package/dist/sdk/json.js +31 -0
  66. package/dist/sdk/json.js.map +1 -0
  67. package/dist/sdk/loader.d.ts +10 -0
  68. package/dist/sdk/loader.d.ts.map +1 -0
  69. package/dist/sdk/loader.js +75 -0
  70. package/dist/sdk/loader.js.map +1 -0
  71. package/dist/sdk/sdk.d.ts +102 -0
  72. package/dist/sdk/sdk.d.ts.map +1 -0
  73. package/dist/sdk/sdk.js +669 -0
  74. package/dist/sdk/sdk.js.map +1 -0
  75. package/dist/sdk/sqlite.d.ts +8 -0
  76. package/dist/sdk/sqlite.d.ts.map +1 -0
  77. package/dist/sdk/sqlite.js +288 -0
  78. package/dist/sdk/sqlite.js.map +1 -0
  79. package/dist/signal.d.ts +17 -0
  80. package/dist/signal.d.ts.map +1 -0
  81. package/dist/signal.js +78 -0
  82. package/dist/signal.js.map +1 -0
  83. package/dist/testing/index.d.ts +35 -0
  84. package/dist/testing/index.d.ts.map +1 -0
  85. package/dist/testing/index.js +244 -0
  86. package/dist/testing/index.js.map +1 -0
  87. package/package.json +67 -0
  88. package/src/authoring.ts +32 -0
  89. package/src/cli/integrations.ts +75 -0
  90. package/src/cli/main.ts +793 -0
  91. package/src/core.ts +1515 -0
  92. package/src/errors.ts +19 -0
  93. package/src/events.ts +37 -0
  94. package/src/index.ts +109 -0
  95. package/src/replay.ts +29 -0
  96. package/src/runtime.ts +314 -0
  97. package/src/schema.ts +34 -0
  98. package/src/schemas.ts +473 -0
  99. package/src/sdk/artifact.ts +176 -0
  100. package/src/sdk/graph.ts +486 -0
  101. package/src/sdk/index.ts +64 -0
  102. package/src/sdk/integrations.ts +49 -0
  103. package/src/sdk/json.ts +40 -0
  104. package/src/sdk/loader.ts +99 -0
  105. package/src/sdk/sdk.ts +911 -0
  106. package/src/sdk/sqlite.ts +405 -0
  107. package/src/signal.ts +116 -0
  108. 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
+ }