@classytic/streamline 1.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.
@@ -0,0 +1,12 @@
1
+ import { _ as WorkflowHandlers, h as WorkflowDefinition } from "../types-DG85_LzF.mjs";
2
+
3
+ //#region src/integrations/fastify.d.ts
4
+ interface WorkflowPluginOptions {
5
+ workflows: Array<{
6
+ definition: WorkflowDefinition;
7
+ handlers: WorkflowHandlers;
8
+ }>;
9
+ }
10
+ declare function workflowPlugin(fastify: unknown, options: WorkflowPluginOptions): Promise<void>;
11
+ //#endregion
12
+ export { workflowPlugin as default };
@@ -0,0 +1,23 @@
1
+ import { p as WorkflowEngine, t as createContainer } from "../container-BzpIMrrj.mjs";
2
+
3
+ //#region src/integrations/fastify.ts
4
+ async function workflowPlugin(fastify, options) {
5
+ const engines = /* @__PURE__ */ new Map();
6
+ const fastifyInstance = fastify;
7
+ for (const { definition, handlers } of options.workflows) {
8
+ const engine = new WorkflowEngine(definition, handlers, createContainer());
9
+ engines.set(definition.id, engine);
10
+ }
11
+ fastifyInstance.decorate("workflows", engines);
12
+ fastifyInstance.decorate("getWorkflow", (id) => {
13
+ const engine = engines.get(id);
14
+ if (!engine) throw new Error(`Workflow ${id} not found`);
15
+ return engine;
16
+ });
17
+ fastifyInstance.addHook("onClose", async () => {
18
+ for (const engine of engines.values()) engine.shutdown();
19
+ });
20
+ }
21
+
22
+ //#endregion
23
+ export { workflowPlugin as default };
@@ -0,0 +1,18 @@
1
+ import { t as WorkflowEventBus } from "../events-C0sZINZq.mjs";
2
+ import { Tracer } from "@opentelemetry/api";
3
+
4
+ //#region src/telemetry/index.d.ts
5
+ interface TelemetryConfig {
6
+ tracer: Tracer;
7
+ /** Event bus to listen to (defaults to globalEventBus) */
8
+ eventBus?: WorkflowEventBus;
9
+ }
10
+ declare function enableTelemetry(config: TelemetryConfig): void;
11
+ declare function disableTelemetry(): void;
12
+ declare function isTelemetryEnabled(): boolean;
13
+ /**
14
+ * Get the current event bus used by telemetry (for debugging)
15
+ */
16
+ declare function getTelemetryEventBus(): WorkflowEventBus | null;
17
+ //#endregion
18
+ export { disableTelemetry, enableTelemetry, getTelemetryEventBus, isTelemetryEnabled };
@@ -0,0 +1,102 @@
1
+ import { n as globalEventBus } from "../events-B5aTz7kD.mjs";
2
+
3
+ //#region src/telemetry/index.ts
4
+ const spans = /* @__PURE__ */ new Map();
5
+ const listeners = [];
6
+ let currentEventBus = null;
7
+ let enabled = false;
8
+ function enableTelemetry(config) {
9
+ if (enabled) return;
10
+ const { tracer } = config;
11
+ const eventBus = config.eventBus ?? globalEventBus;
12
+ currentEventBus = eventBus;
13
+ enabled = true;
14
+ const on = (event, handler) => {
15
+ const fn = (...args) => handler(args[0]);
16
+ listeners.push({
17
+ event,
18
+ fn,
19
+ bus: eventBus
20
+ });
21
+ eventBus.on(event, fn);
22
+ };
23
+ on("workflow:started", ({ runId }) => {
24
+ if (!runId) return;
25
+ const span = tracer.startSpan(`workflow:${runId}`);
26
+ span.setAttribute("workflow.runId", runId);
27
+ spans.set(runId, span);
28
+ });
29
+ on("step:started", ({ runId, stepId }) => {
30
+ if (!runId || !stepId) return;
31
+ const span = tracer.startSpan(`step:${stepId}`);
32
+ span.setAttribute("workflow.runId", runId);
33
+ span.setAttribute("step.id", stepId);
34
+ spans.set(`${runId}:${stepId}`, span);
35
+ });
36
+ on("step:completed", ({ runId, stepId }) => {
37
+ if (!runId || !stepId) return;
38
+ const span = spans.get(`${runId}:${stepId}`);
39
+ if (span) {
40
+ span.setStatus({ code: 1 });
41
+ span.end();
42
+ spans.delete(`${runId}:${stepId}`);
43
+ }
44
+ });
45
+ on("step:failed", ({ runId, stepId, data }) => {
46
+ if (!runId || !stepId) return;
47
+ const span = spans.get(`${runId}:${stepId}`);
48
+ if (span) {
49
+ const errorData = data;
50
+ span.setStatus({
51
+ code: 2,
52
+ message: errorData?.error?.message
53
+ });
54
+ if (errorData?.error) span.recordException(errorData.error);
55
+ span.end();
56
+ spans.delete(`${runId}:${stepId}`);
57
+ }
58
+ });
59
+ on("workflow:completed", ({ runId }) => {
60
+ if (!runId) return;
61
+ const span = spans.get(runId);
62
+ if (span) {
63
+ span.setStatus({ code: 1 });
64
+ span.end();
65
+ spans.delete(runId);
66
+ }
67
+ });
68
+ on("workflow:failed", ({ runId, data }) => {
69
+ if (!runId) return;
70
+ const span = spans.get(runId);
71
+ if (span) {
72
+ const errorData = data;
73
+ span.setStatus({
74
+ code: 2,
75
+ message: errorData?.error?.message
76
+ });
77
+ if (errorData?.error) span.recordException(errorData.error);
78
+ span.end();
79
+ spans.delete(runId);
80
+ }
81
+ });
82
+ }
83
+ function disableTelemetry() {
84
+ if (!enabled) return;
85
+ listeners.forEach(({ event, fn, bus }) => bus.off(event, fn));
86
+ listeners.length = 0;
87
+ spans.clear();
88
+ currentEventBus = null;
89
+ enabled = false;
90
+ }
91
+ function isTelemetryEnabled() {
92
+ return enabled;
93
+ }
94
+ /**
95
+ * Get the current event bus used by telemetry (for debugging)
96
+ */
97
+ function getTelemetryEventBus() {
98
+ return currentEventBus;
99
+ }
100
+
101
+ //#endregion
102
+ export { disableTelemetry, enableTelemetry, getTelemetryEventBus, isTelemetryEnabled };
@@ -0,0 +1,275 @@
1
+ //#region src/core/types.d.ts
2
+ type StepStatus = 'pending' | 'running' | 'waiting' | 'done' | 'failed' | 'skipped';
3
+ type RunStatus = 'draft' | 'running' | 'waiting' | 'done' | 'failed' | 'cancelled';
4
+ interface Step {
5
+ id: string;
6
+ name: string;
7
+ description?: string;
8
+ /**
9
+ * Maximum number of execution attempts for this step (including the initial attempt).
10
+ *
11
+ * Example: retries=3 means:
12
+ * - Attempt 1 (initial execution)
13
+ * - Attempt 2 (first retry after failure)
14
+ * - Attempt 3 (second retry after failure)
15
+ * - Total: 3 attempts
16
+ *
17
+ * If all attempts fail, the step is marked as 'failed' and the workflow stops.
18
+ * Uses exponential backoff: 1s, 2s, 4s, 8s, ... (max 60s between retries).
19
+ *
20
+ * @default 3
21
+ */
22
+ retries?: number;
23
+ /**
24
+ * Maximum execution time in milliseconds for this step.
25
+ * If the step handler doesn't complete within this time, it throws a timeout error.
26
+ *
27
+ * @default undefined (no timeout)
28
+ */
29
+ timeout?: number;
30
+ /**
31
+ * Full condition function with access to context and run.
32
+ * Return true to execute the step, false to skip.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * step({
37
+ * id: 'send-email',
38
+ * name: 'Send Email',
39
+ * condition: (context, run) => context.shouldSendEmail && run.status === 'running'
40
+ * })
41
+ * ```
42
+ */
43
+ condition?: (context: unknown, run: WorkflowRun) => boolean | Promise<boolean>;
44
+ /**
45
+ * Skip this step if the predicate returns true.
46
+ * Simpler alternative to condition for basic skip logic.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * step({ id: 'optional-step', name: 'Optional', skipIf: (ctx) => !ctx.featureEnabled })
51
+ * ```
52
+ */
53
+ skipIf?: (context: unknown) => boolean | Promise<boolean>;
54
+ /**
55
+ * Only run this step if the predicate returns true.
56
+ * Simpler alternative to condition for basic run logic.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * step({ id: 'premium-feature', name: 'Premium', runIf: (ctx) => ctx.isPremiumUser })
61
+ * ```
62
+ */
63
+ runIf?: (context: unknown) => boolean | Promise<boolean>;
64
+ }
65
+ interface StepError {
66
+ message: string;
67
+ code?: string;
68
+ retriable?: boolean;
69
+ stack?: string;
70
+ }
71
+ interface WorkflowError {
72
+ message: string;
73
+ code?: string;
74
+ stack?: string;
75
+ }
76
+ interface WaitingFor {
77
+ type: 'human' | 'webhook' | 'timer' | 'event';
78
+ reason: string;
79
+ resumeAt?: Date;
80
+ eventName?: string;
81
+ data?: unknown;
82
+ }
83
+ interface StepState<TOutput = unknown> {
84
+ stepId: string;
85
+ status: StepStatus;
86
+ attempts: number;
87
+ startedAt?: Date;
88
+ endedAt?: Date;
89
+ output?: TOutput;
90
+ waitingFor?: WaitingFor;
91
+ error?: StepError;
92
+ retryAfter?: Date;
93
+ }
94
+ /**
95
+ * Scheduling metadata for timezone-aware workflow execution
96
+ */
97
+ interface SchedulingInfo {
98
+ /**
99
+ * User's intended local time as ISO string (without timezone suffix)
100
+ * Format: "YYYY-MM-DDTHH:mm:ss" (e.g., "2024-03-10T09:00:00")
101
+ * This is the ORIGINAL string the user provided, preserved for accurate rescheduling
102
+ */
103
+ scheduledFor: string;
104
+ /** IANA timezone name (e.g., "America/New_York", "Europe/London") */
105
+ timezone: string;
106
+ /** Human-readable local time with timezone abbreviation (e.g., "2024-03-10 09:00:00 EDT") */
107
+ localTimeDisplay: string;
108
+ /** UTC execution time - used by scheduler for actual execution */
109
+ executionTime: Date;
110
+ /** Whether this time falls during a DST transition */
111
+ isDSTTransition: boolean;
112
+ /** Human-readable note about DST adjustments (if any) */
113
+ dstNote?: string;
114
+ /** Optional recurrence pattern for repeating workflows */
115
+ recurrence?: RecurrencePattern;
116
+ }
117
+ /**
118
+ * Recurrence pattern for scheduled workflows
119
+ */
120
+ interface RecurrencePattern {
121
+ /** How often to repeat (daily, weekly, monthly, custom cron) */
122
+ pattern: 'daily' | 'weekly' | 'monthly' | 'custom';
123
+ /** For weekly: which days (0=Sunday, 6=Saturday) */
124
+ daysOfWeek?: number[];
125
+ /** For monthly: which day of month (1-31) */
126
+ dayOfMonth?: number;
127
+ /** Custom cron expression (if pattern='custom') */
128
+ cronExpression?: string;
129
+ /** Stop repeating after this date */
130
+ until?: Date;
131
+ /** Or stop after N occurrences */
132
+ count?: number;
133
+ /** How many times has this recurred so far */
134
+ occurrences?: number;
135
+ }
136
+ interface WorkflowRun<TContext = Record<string, unknown>> {
137
+ _id: string;
138
+ workflowId: string;
139
+ status: RunStatus;
140
+ steps: StepState[];
141
+ currentStepId: string | null;
142
+ context: TContext;
143
+ input: unknown;
144
+ output?: unknown;
145
+ error?: WorkflowError;
146
+ createdAt: Date;
147
+ updatedAt: Date;
148
+ startedAt?: Date;
149
+ endedAt?: Date;
150
+ lastHeartbeat?: Date;
151
+ paused?: boolean;
152
+ /** Timezone-aware scheduling metadata (optional - only for scheduled workflows) */
153
+ scheduling?: SchedulingInfo;
154
+ userId?: string;
155
+ tags?: string[];
156
+ meta?: Record<string, unknown>;
157
+ }
158
+ interface WorkflowDefinition<TContext = Record<string, unknown>> {
159
+ id: string;
160
+ name: string;
161
+ version: string;
162
+ steps: Step[];
163
+ createContext: (input: unknown) => TContext;
164
+ /**
165
+ * Default values for all steps in this workflow.
166
+ * Individual steps can override these defaults.
167
+ */
168
+ defaults?: {
169
+ /**
170
+ * Maximum number of execution attempts for each step (including initial attempt).
171
+ * @default 3
172
+ */
173
+ retries?: number;
174
+ /**
175
+ * Maximum execution time in milliseconds for each step.
176
+ * @default undefined (no timeout)
177
+ */
178
+ timeout?: number;
179
+ };
180
+ }
181
+ interface StepContext<TContext = Record<string, unknown>> {
182
+ runId: string;
183
+ stepId: string;
184
+ context: TContext;
185
+ input: unknown;
186
+ attempt: number;
187
+ /**
188
+ * AbortSignal for step cancellation.
189
+ * Handlers should check this signal and abort long-running operations when triggered.
190
+ * The signal is aborted when:
191
+ * - Step timeout is exceeded
192
+ * - Workflow is cancelled
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * async function fetchData(ctx) {
197
+ * const response = await fetch(url, { signal: ctx.signal });
198
+ * // ...
199
+ * }
200
+ * ```
201
+ */
202
+ signal: AbortSignal;
203
+ set: <K extends keyof TContext>(key: K, value: TContext[K]) => Promise<void>;
204
+ getOutput: <T = unknown>(stepId: string) => T | undefined;
205
+ wait: (reason: string, data?: unknown) => Promise<never>;
206
+ waitFor: (eventName: string, reason?: string) => Promise<unknown>;
207
+ sleep: (ms: number) => Promise<void>;
208
+ /**
209
+ * Send a heartbeat to prevent the workflow from being marked as stale.
210
+ * Use this in long-running steps (5+ minutes) to signal the step is still active.
211
+ *
212
+ * Heartbeats are automatically sent every 30 seconds during step execution,
213
+ * but you can call this manually for extra control.
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * async function processLargeDataset(ctx) {
218
+ * for (const batch of batches) {
219
+ * await processBatch(batch);
220
+ * await ctx.heartbeat(); // Signal we're still alive
221
+ * }
222
+ * }
223
+ * ```
224
+ */
225
+ heartbeat: () => Promise<void>;
226
+ emit: (eventName: string, data: unknown) => void;
227
+ log: (message: string, data?: unknown) => void;
228
+ }
229
+ type StepHandler<TOutput = unknown, TContext = Record<string, unknown>> = (ctx: StepContext<TContext>) => Promise<TOutput>;
230
+ type WorkflowHandlers<TContext = Record<string, unknown>> = {
231
+ [stepId: string]: StepHandler<unknown, TContext>;
232
+ };
233
+ /**
234
+ * Infer context type from WorkflowDefinition
235
+ * @example
236
+ * type MyContext = InferContext<typeof myWorkflow>
237
+ */
238
+ type InferContext<T> = T extends WorkflowDefinition<infer TContext> ? TContext : never;
239
+ /**
240
+ * Infer context type from WorkflowHandlers
241
+ * @example
242
+ * type MyContext = InferHandlersContext<typeof myHandlers>
243
+ */
244
+ type InferHandlersContext<T> = T extends WorkflowHandlers<infer TContext> ? TContext : never;
245
+ /**
246
+ * Strongly-typed handlers that match workflow steps
247
+ * Ensures all step IDs have corresponding handlers
248
+ * @example
249
+ * const handlers: TypedHandlers<typeof workflow, MyContext> = { ... }
250
+ */
251
+ type TypedHandlers<TWorkflow extends WorkflowDefinition<any>, TContext = InferContext<TWorkflow>> = { [K in TWorkflow['steps'][number]['id']]: StepHandler<unknown, TContext> };
252
+ /**
253
+ * Extract step IDs as union type from workflow definition
254
+ * @example
255
+ * type MyStepIds = StepIds<typeof myWorkflow> // 'step1' | 'step2' | 'step3'
256
+ */
257
+ type StepIds<T extends WorkflowDefinition<any>> = T['steps'][number]['id'];
258
+ /**
259
+ * Payload for workflow and engine events
260
+ */
261
+ interface WorkflowEventPayload {
262
+ runId?: string;
263
+ stepId?: string;
264
+ data?: unknown;
265
+ error?: Error;
266
+ context?: string;
267
+ /**
268
+ * Explicit broadcast flag for resuming multiple workflows.
269
+ * When true, the event will resume ALL workflows waiting on this event.
270
+ * When false/undefined with no runId, a warning is logged.
271
+ */
272
+ broadcast?: boolean;
273
+ }
274
+ //#endregion
275
+ export { WorkflowHandlers as _, SchedulingInfo as a, StepError as c, StepState as d, StepStatus as f, WorkflowEventPayload as g, WorkflowDefinition as h, RunStatus as i, StepHandler as l, WaitingFor as m, InferHandlersContext as n, Step as o, TypedHandlers as p, RecurrencePattern as r, StepContext as s, InferContext as t, StepIds as u, WorkflowRun as v };
package/package.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "@classytic/streamline",
3
+ "version": "1.0.0",
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
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.mjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "./fastify": {
16
+ "types": "./dist/integrations/fastify.d.mts",
17
+ "default": "./dist/integrations/fastify.mjs"
18
+ },
19
+ "./telemetry": {
20
+ "types": "./dist/telemetry/index.d.mts",
21
+ "default": "./dist/telemetry/index.mjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsdown",
31
+ "dev": "tsdown --watch",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
36
+ },
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "keywords": [
41
+ "workflow",
42
+ "temporal",
43
+ "durable",
44
+ "durable-execution",
45
+ "orchestration",
46
+ "mongodb",
47
+ "mongoose",
48
+ "sleep",
49
+ "wait",
50
+ "resume",
51
+ "human-in-the-loop",
52
+ "background-jobs",
53
+ "state-machine",
54
+ "crash-recovery",
55
+ "retry",
56
+ "parallel",
57
+ "scheduling",
58
+ "webhook",
59
+ "approval-flow",
60
+ "typescript"
61
+ ],
62
+ "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
63
+ "contributors": [
64
+ "Sadman Chowdhury (https://github.com/siam923)"
65
+ ],
66
+ "license": "MIT",
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "https://github.com/classytic/streamline.git"
70
+ },
71
+ "homepage": "https://github.com/classytic/streamline#readme",
72
+ "bugs": {
73
+ "url": "https://github.com/classytic/streamline/issues"
74
+ },
75
+ "peerDependencies": {
76
+ "@classytic/mongokit": "^3.2.3",
77
+ "mongoose": "^9.0.0",
78
+ "@opentelemetry/api": ">=1.0.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@opentelemetry/api": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "devDependencies": {
86
+ "@classytic/mongokit": "^3.2.3",
87
+ "@opentelemetry/api": "^1.9.0",
88
+ "@types/luxon": "^3.7.1",
89
+ "luxon": "^3.7.2",
90
+ "semver": "^7.7.3",
91
+ "@types/node": "^22.0.0",
92
+ "@types/semver": "^7.7.1",
93
+ "@vitest/coverage-v8": "^3.0.0",
94
+ "mongodb-memory-server": "^11.0.1",
95
+ "mongoose": "^9.0.0",
96
+ "tsdown": "^0.20.3",
97
+ "typescript": "^5.0.0",
98
+ "vitest": "^3.0.0"
99
+ },
100
+ "dependencies": {
101
+ "luxon": "^3.0.0",
102
+ "semver": "^7.0.0"
103
+ }
104
+ }