@classytic/streamline 1.0.0 → 2.0.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/README.md +66 -23
- package/dist/{container-BzpIMrrj.mjs → container-BSxj_Or4.mjs} +294 -165
- package/dist/context-DMkusPr_.mjs +224 -0
- package/dist/{errors-BqunvWPz.mjs → errors-Ba7ZziTN.mjs} +1 -16
- package/dist/{events-C0sZINZq.d.mts → events-DC0ddZZ9.d.mts} +1 -1
- package/dist/index.d.mts +179 -51
- package/dist/index.mjs +140 -43
- package/dist/integrations/fastify.d.mts +1 -1
- package/dist/integrations/fastify.mjs +1 -1
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/telemetry/index.d.mts +1 -1
- package/dist/telemetry/index.mjs +1 -1
- package/dist/{types-DG85_LzF.d.mts → types-DjgzSrNY.d.mts} +111 -1
- package/package.json +11 -10
- /package/dist/{events-B5aTz7kD.mjs → events-CpesEn3I.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { A as
|
|
3
|
-
import { n as
|
|
1
|
+
import { a as logger, r as WaitSignal, t as GotoSignal } from "./context-DMkusPr_.mjs";
|
|
2
|
+
import { A as isValidRunTransition, C as shouldSkipStep, D as isRunStatus, E as deriveRunStatus, O as isStepStatus, S as isConditionalStep, T as STEP_STATUS_VALUES, _ as validateRetryConfig, a as singleTenantPlugin, b as conditions, c as RUN_STATUS, d as WorkflowRunModel, f as WorkflowCache, g as validateId, h as workflowRegistry, i as workflowRunRepository, j as isValidStepTransition, k as isTerminalState, l as STEP_STATUS, m as hookRegistry, n as isStreamlineContainer, o as tenantFilterPlugin, p as WorkflowEngine, r as createWorkflowRepository, s as CommonQueries, t as createContainer, u as WorkflowQueryBuilder, v as COMPUTED, w as RUN_STATUS_VALUES, x as createCondition, y as SCHEDULING } from "./container-BSxj_Or4.mjs";
|
|
3
|
+
import { a as StepNotFoundError, c as WorkflowNotFoundError, i as MaxRetriesExceededError, n as ErrorCode, o as StepTimeoutError, r as InvalidStateError, s as WorkflowError, t as DataCorruptionError } from "./errors-Ba7ZziTN.mjs";
|
|
4
|
+
import { n as globalEventBus, t as WorkflowEventBus } from "./events-CpesEn3I.mjs";
|
|
4
5
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
6
|
import mongoose, { Schema } from "mongoose";
|
|
6
7
|
import semver from "semver";
|
|
@@ -39,6 +40,30 @@ import { DateTime } from "luxon";
|
|
|
39
40
|
* ```
|
|
40
41
|
*/
|
|
41
42
|
const toName = (id) => id.replace(/-/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
43
|
+
/** Type guard: is the step entry a StepConfig object (vs a plain handler fn)? */
|
|
44
|
+
function isStepConfig(step) {
|
|
45
|
+
return typeof step === "object" && step !== null && "handler" in step;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Convert a StepConfig entry into a Step definition.
|
|
49
|
+
*
|
|
50
|
+
* The Step interface uses `(context: unknown)` for conditions because steps are
|
|
51
|
+
* stored in WorkflowDefinition (untyped at runtime). This is the one boundary
|
|
52
|
+
* where typed `TContext` callbacks widen to `unknown` — safe because the executor
|
|
53
|
+
* always passes the correct TContext at call sites.
|
|
54
|
+
*/
|
|
55
|
+
function toStepDef(stepId, entry) {
|
|
56
|
+
const step = {
|
|
57
|
+
id: stepId,
|
|
58
|
+
name: toName(stepId)
|
|
59
|
+
};
|
|
60
|
+
if (entry.timeout !== void 0) step.timeout = entry.timeout;
|
|
61
|
+
if (entry.retries !== void 0) step.retries = entry.retries;
|
|
62
|
+
if (entry.condition) step.condition = entry.condition;
|
|
63
|
+
if (entry.skipIf) step.skipIf = entry.skipIf;
|
|
64
|
+
if (entry.runIf) step.runIf = entry.runIf;
|
|
65
|
+
return step;
|
|
66
|
+
}
|
|
42
67
|
/**
|
|
43
68
|
* Create a workflow with inline step handlers
|
|
44
69
|
*
|
|
@@ -57,12 +82,17 @@ const toName = (id) => id.replace(/-/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
|
57
82
|
* await orderProcess.start({ id: '123', email: 'user@example.com' });
|
|
58
83
|
* ```
|
|
59
84
|
*
|
|
60
|
-
* @example
|
|
85
|
+
* @example Per-step configuration
|
|
61
86
|
* ```typescript
|
|
62
|
-
* const
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
87
|
+
* const pipeline = createWorkflow('ci-pipeline', {
|
|
88
|
+
* steps: {
|
|
89
|
+
* clone: { handler: async (ctx) => { ... }, timeout: 120_000 },
|
|
90
|
+
* build: { handler: async (ctx) => { ... }, retries: 5 },
|
|
91
|
+
* deploy: {
|
|
92
|
+
* handler: async (ctx) => { ... },
|
|
93
|
+
* skipIf: (ctx) => !ctx.shouldDeploy,
|
|
94
|
+
* },
|
|
95
|
+
* },
|
|
66
96
|
* });
|
|
67
97
|
* ```
|
|
68
98
|
*/
|
|
@@ -71,19 +101,34 @@ function createWorkflow(id, config) {
|
|
|
71
101
|
const stepIds = Object.keys(config.steps);
|
|
72
102
|
if (stepIds.length === 0) throw new Error("Workflow must have at least one step");
|
|
73
103
|
if (config.defaults) validateRetryConfig(config.defaults.retries, config.defaults.timeout);
|
|
104
|
+
const handlers = {};
|
|
105
|
+
const compensationHandlers = {};
|
|
106
|
+
const steps = stepIds.map((stepId) => {
|
|
107
|
+
const entry = config.steps[stepId];
|
|
108
|
+
if (isStepConfig(entry)) {
|
|
109
|
+
handlers[stepId] = entry.handler;
|
|
110
|
+
if (entry.onCompensate) compensationHandlers[stepId] = entry.onCompensate;
|
|
111
|
+
return toStepDef(stepId, entry);
|
|
112
|
+
}
|
|
113
|
+
handlers[stepId] = entry;
|
|
114
|
+
return {
|
|
115
|
+
id: stepId,
|
|
116
|
+
name: toName(stepId)
|
|
117
|
+
};
|
|
118
|
+
});
|
|
74
119
|
const definition = {
|
|
75
120
|
id,
|
|
76
121
|
name: toName(id),
|
|
77
122
|
version: config.version ?? "1.0.0",
|
|
78
|
-
steps
|
|
79
|
-
id: stepId,
|
|
80
|
-
name: toName(stepId)
|
|
81
|
-
})),
|
|
123
|
+
steps,
|
|
82
124
|
createContext: config.context ?? ((input) => input),
|
|
83
125
|
defaults: config.defaults
|
|
84
126
|
};
|
|
85
127
|
const container = config.container ?? createContainer();
|
|
86
|
-
const engine = new WorkflowEngine(definition,
|
|
128
|
+
const engine = new WorkflowEngine(definition, handlers, container, {
|
|
129
|
+
...config.autoExecute !== void 0 && { autoExecute: config.autoExecute },
|
|
130
|
+
compensationHandlers: Object.keys(compensationHandlers).length > 0 ? compensationHandlers : void 0
|
|
131
|
+
});
|
|
87
132
|
const waitFor = async (runId, options = {}) => {
|
|
88
133
|
const { pollInterval = 1e3, timeout } = options;
|
|
89
134
|
const startTime = Date.now();
|
|
@@ -114,17 +159,20 @@ function createWorkflow(id, config) {
|
|
|
114
159
|
//#endregion
|
|
115
160
|
//#region src/features/hooks.ts
|
|
116
161
|
/**
|
|
117
|
-
* Hooks & Webhooks
|
|
162
|
+
* Hooks & Webhooks — Durable external resume
|
|
118
163
|
*
|
|
119
|
-
*
|
|
164
|
+
* Pause execution and wait for external input (webhooks, approvals, etc.).
|
|
165
|
+
* Durable across process restarts and multi-worker deployments:
|
|
166
|
+
* - Fast path: in-memory hookRegistry (same-process)
|
|
167
|
+
* - Fallback: MongoDB lookup + repository-based resume (cross-process)
|
|
120
168
|
*
|
|
121
169
|
* @example
|
|
122
170
|
* ```typescript
|
|
123
171
|
* const approval = createWorkflow('doc-approval', {
|
|
124
172
|
* steps: {
|
|
125
173
|
* request: async (ctx) => {
|
|
126
|
-
*
|
|
127
|
-
* return
|
|
174
|
+
* const hook = createHook(ctx, 'approval');
|
|
175
|
+
* return ctx.wait(hook.token, { hookToken: hook.token });
|
|
128
176
|
* },
|
|
129
177
|
* process: async (ctx) => {
|
|
130
178
|
* const { approved } = ctx.getOutput<{ approved: boolean }>('request');
|
|
@@ -133,32 +181,18 @@ function createWorkflow(id, config) {
|
|
|
133
181
|
* },
|
|
134
182
|
* });
|
|
135
183
|
*
|
|
136
|
-
* // Resume
|
|
184
|
+
* // Resume from API route — works across workers/restarts
|
|
137
185
|
* const result = await resumeHook(token, { approved: true });
|
|
138
|
-
* console.log(result.run); // The resumed workflow run
|
|
139
186
|
* ```
|
|
140
187
|
*/
|
|
141
188
|
/**
|
|
142
189
|
* Create a hook that pauses workflow until external input.
|
|
143
190
|
* The token includes a crypto-random suffix for security.
|
|
144
191
|
*
|
|
145
|
-
* IMPORTANT: Pass the returned token to ctx.wait() to enable token validation:
|
|
146
|
-
* ```typescript
|
|
147
|
-
* const hook = createHook(ctx, 'approval');
|
|
148
|
-
* return ctx.wait(hook.token, { hookToken: hook.token }); // Token stored for validation
|
|
149
|
-
* ```
|
|
150
|
-
*
|
|
151
192
|
* @example
|
|
152
193
|
* ```typescript
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* const hook = createHook(ctx, 'waiting-for-approval');
|
|
156
|
-
* console.log('Resume with token:', hook.token);
|
|
157
|
-
* return ctx.wait(hook.token, { hookToken: hook.token }); // Token validated on resume
|
|
158
|
-
* }
|
|
159
|
-
*
|
|
160
|
-
* // From API route
|
|
161
|
-
* await resumeHook('token-123', { approved: true });
|
|
194
|
+
* const hook = createHook(ctx, 'waiting-for-approval');
|
|
195
|
+
* return ctx.wait(hook.token, { hookToken: hook.token });
|
|
162
196
|
* ```
|
|
163
197
|
*/
|
|
164
198
|
function createHook(ctx, reason, options) {
|
|
@@ -172,14 +206,14 @@ function createHook(ctx, reason, options) {
|
|
|
172
206
|
/**
|
|
173
207
|
* Resume a paused workflow by hook token.
|
|
174
208
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
209
|
+
* **Durable**: Works across process restarts and multi-worker deployments.
|
|
210
|
+
* - Fast path: Uses in-memory hookRegistry if the engine is in this process.
|
|
211
|
+
* - Fallback: Looks up the workflow in MongoDB and resumes via atomic DB operations.
|
|
177
212
|
*
|
|
178
|
-
*
|
|
213
|
+
* Security: Validates the token against the stored hookToken if present.
|
|
179
214
|
*
|
|
180
215
|
* @example
|
|
181
216
|
* ```typescript
|
|
182
|
-
* // API route handler
|
|
183
217
|
* app.post('/hooks/:token', async (req, res) => {
|
|
184
218
|
* const result = await resumeHook(req.params.token, req.body);
|
|
185
219
|
* res.json({ success: true, runId: result.runId, status: result.run.status });
|
|
@@ -190,12 +224,15 @@ async function resumeHook(token, payload) {
|
|
|
190
224
|
const [runId] = token.split(":");
|
|
191
225
|
if (!runId) throw new Error(`Invalid hook token: ${token}`);
|
|
192
226
|
const engine = hookRegistry.getEngine(runId);
|
|
193
|
-
if (
|
|
227
|
+
if (engine) return resumeViaEngine(engine, runId, token, payload);
|
|
228
|
+
return resumeViaDb(runId, token, payload);
|
|
229
|
+
}
|
|
230
|
+
/** Resume using the in-memory engine reference (fast path) */
|
|
231
|
+
async function resumeViaEngine(engine, runId, token, payload) {
|
|
194
232
|
const run = await engine.container.repository.getById(runId);
|
|
195
233
|
if (!run) throw new Error(`Workflow not found for token: ${token}`);
|
|
196
234
|
if (run.status !== "waiting") throw new Error(`Workflow ${runId} is not waiting (status: ${run.status})`);
|
|
197
|
-
|
|
198
|
-
if (storedToken && storedToken !== token) throw new Error(`Invalid hook token for workflow ${runId}`);
|
|
235
|
+
validateHookToken(run, token);
|
|
199
236
|
const resumedRun = await engine.resume(runId, payload);
|
|
200
237
|
return {
|
|
201
238
|
runId: run._id,
|
|
@@ -203,11 +240,71 @@ async function resumeHook(token, payload) {
|
|
|
203
240
|
};
|
|
204
241
|
}
|
|
205
242
|
/**
|
|
243
|
+
* Resume using direct MongoDB operations (durable fallback).
|
|
244
|
+
* Works when the engine that started the workflow is gone (restart, different worker).
|
|
245
|
+
*/
|
|
246
|
+
async function resumeViaDb(runId, token, payload) {
|
|
247
|
+
const run = await workflowRunRepository.getById(runId);
|
|
248
|
+
if (!run) throw new Error(`Workflow not found for token: ${token}`);
|
|
249
|
+
if (run.status !== "waiting") throw new Error(`Workflow ${runId} is not waiting (status: ${run.status})`);
|
|
250
|
+
validateHookToken(run, token);
|
|
251
|
+
const stepIndex = run.steps.findIndex((s) => s.status === "waiting");
|
|
252
|
+
if (stepIndex === -1) throw new Error(`No waiting step found in workflow ${runId}`);
|
|
253
|
+
const now = /* @__PURE__ */ new Date();
|
|
254
|
+
const stepId = run.steps[stepIndex].stepId;
|
|
255
|
+
if ((await workflowRunRepository.updateOne({
|
|
256
|
+
_id: runId,
|
|
257
|
+
status: "waiting",
|
|
258
|
+
[`steps.${stepIndex}.status`]: "waiting"
|
|
259
|
+
}, {
|
|
260
|
+
$set: {
|
|
261
|
+
status: "running",
|
|
262
|
+
updatedAt: now,
|
|
263
|
+
lastHeartbeat: now,
|
|
264
|
+
[`steps.${stepIndex}.status`]: "done",
|
|
265
|
+
[`steps.${stepIndex}.endedAt`]: now,
|
|
266
|
+
[`steps.${stepIndex}.output`]: payload
|
|
267
|
+
},
|
|
268
|
+
$unset: { [`steps.${stepIndex}.waitingFor`]: "" }
|
|
269
|
+
}, { bypassTenant: true })).modifiedCount === 0) throw new Error(`Failed to resume workflow ${runId} — already resumed or cancelled`);
|
|
270
|
+
const allStepIds = run.steps.map((s) => s.stepId);
|
|
271
|
+
const currentIndex = allStepIds.indexOf(stepId);
|
|
272
|
+
const nextStepId = currentIndex < allStepIds.length - 1 ? allStepIds[currentIndex + 1] : null;
|
|
273
|
+
if (nextStepId) await workflowRunRepository.updateOne({ _id: runId }, { $set: {
|
|
274
|
+
currentStepId: nextStepId,
|
|
275
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
276
|
+
} }, { bypassTenant: true });
|
|
277
|
+
else await workflowRunRepository.updateOne({ _id: runId }, { $set: {
|
|
278
|
+
status: "done",
|
|
279
|
+
currentStepId: null,
|
|
280
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
281
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
282
|
+
output: payload
|
|
283
|
+
} }, { bypassTenant: true });
|
|
284
|
+
const engine = workflowRegistry.getEngine(run.workflowId);
|
|
285
|
+
if (engine) {
|
|
286
|
+
engine.container.cache.delete(runId);
|
|
287
|
+
if (nextStepId) setImmediate(() => {
|
|
288
|
+
engine.execute(runId).catch(() => {});
|
|
289
|
+
});
|
|
290
|
+
} else if (nextStepId) await workflowRunRepository.updateOne({ _id: runId }, { $set: { lastHeartbeat: /* @__PURE__ */ new Date(0) } }, { bypassTenant: true });
|
|
291
|
+
const updated = await workflowRunRepository.getById(runId);
|
|
292
|
+
if (!updated) throw new Error(`Workflow ${runId} disappeared after resume`);
|
|
293
|
+
return {
|
|
294
|
+
runId,
|
|
295
|
+
run: updated
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Validate hook token against stored token (security) */
|
|
299
|
+
function validateHookToken(run, token) {
|
|
300
|
+
const storedToken = (run.steps.find((s) => s.status === "waiting")?.waitingFor?.data)?.hookToken;
|
|
301
|
+
if (storedToken && storedToken !== token) throw new Error(`Invalid hook token for workflow ${run._id}`);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
206
304
|
* Generate a deterministic token for idempotent hooks
|
|
207
305
|
*
|
|
208
306
|
* @example
|
|
209
307
|
* ```typescript
|
|
210
|
-
* // Slack bot - same channel always gets same token
|
|
211
308
|
* const token = hookToken('slack', channelId);
|
|
212
309
|
* const hook = createHook(ctx, 'slack-message', { token });
|
|
213
310
|
* ```
|
|
@@ -1099,4 +1196,4 @@ async function executeParallel(tasks, options = {}) {
|
|
|
1099
1196
|
}
|
|
1100
1197
|
|
|
1101
1198
|
//#endregion
|
|
1102
|
-
export { COMPUTED, CommonQueries, DataCorruptionError, ErrorCode, InvalidStateError, MaxRetriesExceededError, RUN_STATUS, RUN_STATUS_VALUES, STEP_STATUS, STEP_STATUS_VALUES, SchedulingService, StepNotFoundError, StepTimeoutError, TimezoneHandler, WaitSignal, WorkflowCache, WorkflowDefinitionModel, WorkflowEngine, WorkflowError, WorkflowEventBus, WorkflowNotFoundError, WorkflowQueryBuilder, WorkflowRunModel, canRewindTo, conditions, createCondition, createContainer, createHook, createWorkflow, createWorkflowRepository, deriveRunStatus, executeParallel, getExecutionPath, getStepTimeline, getStepUIStates, getWaitingInfo, getWorkflowProgress, globalEventBus, hookRegistry, hookToken, isConditionalStep, isRunStatus, isStepStatus, isStreamlineContainer, isTerminalState, isValidRunTransition, isValidStepTransition, resumeHook, shouldSkipStep, singleTenantPlugin, tenantFilterPlugin, timezoneHandler, workflowDefinitionRepository, workflowRunRepository };
|
|
1199
|
+
export { COMPUTED, CommonQueries, DataCorruptionError, ErrorCode, GotoSignal, InvalidStateError, MaxRetriesExceededError, RUN_STATUS, RUN_STATUS_VALUES, STEP_STATUS, STEP_STATUS_VALUES, SchedulingService, StepNotFoundError, StepTimeoutError, TimezoneHandler, WaitSignal, WorkflowCache, WorkflowDefinitionModel, WorkflowEngine, WorkflowError, WorkflowEventBus, WorkflowNotFoundError, WorkflowQueryBuilder, WorkflowRunModel, canRewindTo, conditions, createCondition, createContainer, createHook, createWorkflow, createWorkflowRepository, deriveRunStatus, executeParallel, getExecutionPath, getStepTimeline, getStepUIStates, getWaitingInfo, getWorkflowProgress, globalEventBus, hookRegistry, hookToken, isConditionalStep, isRunStatus, isStepStatus, isStreamlineContainer, isTerminalState, isValidRunTransition, isValidStepTransition, resumeHook, shouldSkipStep, singleTenantPlugin, tenantFilterPlugin, timezoneHandler, workflowDefinitionRepository, workflowRegistry, workflowRunRepository };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) {
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!no_symbols) {
|
|
12
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { __exportAll as t };
|
package/dist/telemetry/index.mjs
CHANGED
|
@@ -74,7 +74,7 @@ interface WorkflowError {
|
|
|
74
74
|
stack?: string;
|
|
75
75
|
}
|
|
76
76
|
interface WaitingFor {
|
|
77
|
-
type: 'human' | 'webhook' | 'timer' | 'event';
|
|
77
|
+
type: 'human' | 'webhook' | 'timer' | 'event' | 'childWorkflow';
|
|
78
78
|
reason: string;
|
|
79
79
|
resumeAt?: Date;
|
|
80
80
|
eventName?: string;
|
|
@@ -225,6 +225,116 @@ interface StepContext<TContext = Record<string, unknown>> {
|
|
|
225
225
|
heartbeat: () => Promise<void>;
|
|
226
226
|
emit: (eventName: string, data: unknown) => void;
|
|
227
227
|
log: (message: string, data?: unknown) => void;
|
|
228
|
+
/**
|
|
229
|
+
* Save a checkpoint for crash-safe batch processing (durable loop).
|
|
230
|
+
*
|
|
231
|
+
* On crash recovery, `getCheckpoint()` returns the last saved value,
|
|
232
|
+
* so the step resumes from where it left off instead of restarting.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* async function processBatches(ctx) {
|
|
237
|
+
* const batches = splitIntoBatches(ctx.input.data, 100);
|
|
238
|
+
* const lastProcessed = ctx.getCheckpoint<number>() ?? -1;
|
|
239
|
+
*
|
|
240
|
+
* for (let i = lastProcessed + 1; i < batches.length; i++) {
|
|
241
|
+
* await processBatch(batches[i]);
|
|
242
|
+
* await ctx.checkpoint(i); // Survives crashes
|
|
243
|
+
* await ctx.heartbeat();
|
|
244
|
+
* }
|
|
245
|
+
* return { processed: batches.length };
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
checkpoint: (value: unknown) => Promise<void>;
|
|
250
|
+
/**
|
|
251
|
+
* Read the last checkpoint value saved by `checkpoint()`.
|
|
252
|
+
* Returns `undefined` on first execution (no prior crash).
|
|
253
|
+
*/
|
|
254
|
+
getCheckpoint: <T = unknown>() => T | undefined;
|
|
255
|
+
/**
|
|
256
|
+
* Start a child workflow and durably wait for it to complete.
|
|
257
|
+
*
|
|
258
|
+
* The parent step enters a waiting state. When the child reaches a terminal
|
|
259
|
+
* state (done/failed/cancelled), the parent automatically resumes with the
|
|
260
|
+
* child's output as this step's output.
|
|
261
|
+
*
|
|
262
|
+
* Durable: survives process restarts. The scheduler picks up the parent
|
|
263
|
+
* when the child completes via the `childRunId` stored in waitingFor.
|
|
264
|
+
*
|
|
265
|
+
* @param workflowId - The child workflow's registered ID
|
|
266
|
+
* @param input - Input data for the child workflow
|
|
267
|
+
* @returns The child workflow's output (after it completes)
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* const pipeline = createWorkflow('pipeline', {
|
|
272
|
+
* steps: {
|
|
273
|
+
* validate: async (ctx) => { ... },
|
|
274
|
+
* runSubPipeline: async (ctx) => {
|
|
275
|
+
* return ctx.startChildWorkflow('sub-pipeline', { data: ctx.context.data });
|
|
276
|
+
* // Parent waits here. Resumes when child completes.
|
|
277
|
+
* },
|
|
278
|
+
* finalize: async (ctx) => {
|
|
279
|
+
* const childResult = ctx.getOutput('runSubPipeline');
|
|
280
|
+
* return { done: true, childResult };
|
|
281
|
+
* },
|
|
282
|
+
* },
|
|
283
|
+
* });
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
startChildWorkflow: (workflowId: string, input: unknown) => Promise<never>;
|
|
287
|
+
/**
|
|
288
|
+
* Jump to a different step, breaking out of the linear sequence.
|
|
289
|
+
*
|
|
290
|
+
* Use for conditional branching, error recovery paths, or skip-ahead logic.
|
|
291
|
+
* The target step must exist in the workflow definition.
|
|
292
|
+
*
|
|
293
|
+
* @param stepId - The step ID to jump to
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```typescript
|
|
297
|
+
* const workflow = createWorkflow('order', {
|
|
298
|
+
* steps: {
|
|
299
|
+
* validate: async (ctx) => {
|
|
300
|
+
* if (!ctx.context.paymentValid) {
|
|
301
|
+
* return ctx.goto('handleFailure'); // Skip to failure handler
|
|
302
|
+
* }
|
|
303
|
+
* return { valid: true };
|
|
304
|
+
* },
|
|
305
|
+
* process: async (ctx) => { ... },
|
|
306
|
+
* handleFailure: async (ctx) => { ... },
|
|
307
|
+
* },
|
|
308
|
+
* });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
goto: (stepId: string) => Promise<never>;
|
|
312
|
+
/**
|
|
313
|
+
* Durable scatter/gather — execute tasks in parallel with crash recovery.
|
|
314
|
+
*
|
|
315
|
+
* Unlike `executeParallel()` (in-memory only), `scatter()` persists each
|
|
316
|
+
* task's completion to MongoDB via checkpoints. If the process crashes
|
|
317
|
+
* mid-scatter, only incomplete tasks re-execute on recovery.
|
|
318
|
+
*
|
|
319
|
+
* @param tasks - Named tasks to execute. Keys become result keys.
|
|
320
|
+
* @param options - Concurrency limit (default: Infinity)
|
|
321
|
+
* @returns Record of results keyed by task name
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```typescript
|
|
325
|
+
* const results = await ctx.scatter({
|
|
326
|
+
* user: () => fetchUser(ctx.context.userId),
|
|
327
|
+
* orders: () => fetchOrders(ctx.context.userId),
|
|
328
|
+
* recommendations: () => getRecommendations(ctx.context.userId),
|
|
329
|
+
* });
|
|
330
|
+
*
|
|
331
|
+
* // results.user, results.orders, results.recommendations
|
|
332
|
+
* // If crash after 'user' completes, only 'orders' and 'recommendations' re-run.
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
scatter: <T extends Record<string, () => Promise<unknown>>>(tasks: T, options?: {
|
|
336
|
+
concurrency?: number;
|
|
337
|
+
}) => Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }>;
|
|
228
338
|
}
|
|
229
339
|
type StepHandler<TOutput = unknown, TContext = Record<string, unknown>> = (ctx: StepContext<TContext>) => Promise<TOutput>;
|
|
230
340
|
type WorkflowHandlers<TContext = Record<string, unknown>> = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/streamline",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MongoDB-native durable workflow orchestration engine. Like Temporal but simpler - supports sleep, wait, retry, parallel execution, human-in-the-loop, and crash recovery. Perfect for payment gateways, approval flows, and scheduled tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"./telemetry": {
|
|
20
20
|
"types": "./dist/telemetry/index.d.mts",
|
|
21
21
|
"default": "./dist/telemetry/index.mjs"
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"dist",
|
|
@@ -73,9 +74,9 @@
|
|
|
73
74
|
"url": "https://github.com/classytic/streamline/issues"
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
|
-
"@classytic/mongokit": "
|
|
77
|
-
"
|
|
78
|
-
"
|
|
77
|
+
"@classytic/mongokit": ">=3.3.2",
|
|
78
|
+
"@opentelemetry/api": ">=1.0.0",
|
|
79
|
+
"mongoose": "^9.0.0"
|
|
79
80
|
},
|
|
80
81
|
"peerDependenciesMeta": {
|
|
81
82
|
"@opentelemetry/api": {
|
|
@@ -83,17 +84,17 @@
|
|
|
83
84
|
}
|
|
84
85
|
},
|
|
85
86
|
"devDependencies": {
|
|
86
|
-
"@classytic/mongokit": "
|
|
87
|
+
"@classytic/mongokit": ">=3.3.2",
|
|
87
88
|
"@opentelemetry/api": "^1.9.0",
|
|
88
89
|
"@types/luxon": "^3.7.1",
|
|
89
|
-
"luxon": "^3.7.2",
|
|
90
|
-
"semver": "^7.7.3",
|
|
91
90
|
"@types/node": "^22.0.0",
|
|
92
91
|
"@types/semver": "^7.7.1",
|
|
93
92
|
"@vitest/coverage-v8": "^3.0.0",
|
|
93
|
+
"luxon": "^3.7.2",
|
|
94
94
|
"mongodb-memory-server": "^11.0.1",
|
|
95
|
-
"mongoose": "^9.
|
|
96
|
-
"
|
|
95
|
+
"mongoose": "^9.3.3",
|
|
96
|
+
"semver": "^7.7.3",
|
|
97
|
+
"tsdown": "^0.21.7",
|
|
97
98
|
"typescript": "^5.0.0",
|
|
98
99
|
"vitest": "^3.0.0"
|
|
99
100
|
},
|
|
File without changes
|