@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.
- package/LICENSE +21 -0
- package/README.md +740 -0
- package/dist/container-BzpIMrrj.mjs +2697 -0
- package/dist/errors-BqunvWPz.mjs +129 -0
- package/dist/events-B5aTz7kD.mjs +28 -0
- package/dist/events-C0sZINZq.d.mts +92 -0
- package/dist/index.d.mts +1589 -0
- package/dist/index.mjs +1102 -0
- package/dist/integrations/fastify.d.mts +12 -0
- package/dist/integrations/fastify.mjs +23 -0
- package/dist/telemetry/index.d.mts +18 -0
- package/dist/telemetry/index.mjs +102 -0
- package/dist/types-DG85_LzF.d.mts +275 -0
- package/package.json +104 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,1589 @@
|
|
|
1
|
+
import { _ as WorkflowHandlers, a as SchedulingInfo, c as StepError, d as StepState, f as StepStatus, g as WorkflowEventPayload, h as WorkflowDefinition, i as RunStatus, l as StepHandler, m as WaitingFor, n as InferHandlersContext, o as Step, p as TypedHandlers, r as RecurrencePattern, s as StepContext, t as InferContext, u as StepIds, v as WorkflowRun } from "./types-DG85_LzF.mjs";
|
|
2
|
+
import { n as WorkflowEventName, r as globalEventBus, t as WorkflowEventBus } from "./events-C0sZINZq.mjs";
|
|
3
|
+
import { FilterQuery, Plugin, Repository } from "@classytic/mongokit";
|
|
4
|
+
import mongoose from "mongoose";
|
|
5
|
+
|
|
6
|
+
//#region src/plugins/tenant-filter.plugin.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Tenant filter plugin options
|
|
9
|
+
*/
|
|
10
|
+
interface TenantFilterOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Field path for tenant ID in documents
|
|
13
|
+
* Supports nested fields using dot notation
|
|
14
|
+
*
|
|
15
|
+
* @example 'context.tenantId' → filters { 'context.tenantId': 'tenant-123' }
|
|
16
|
+
* @example 'meta.orgId' → filters { 'meta.orgId': 'org-456' }
|
|
17
|
+
* @default 'context.tenantId'
|
|
18
|
+
*/
|
|
19
|
+
tenantField?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Static tenant ID for single-tenant deployments
|
|
22
|
+
* If set, all queries use this tenant ID (no need to pass tenantId param)
|
|
23
|
+
*
|
|
24
|
+
* @default undefined (multi-tenant mode - tenantId required per query)
|
|
25
|
+
*/
|
|
26
|
+
staticTenantId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Strict mode - throws error if tenantId is missing and not bypassed
|
|
29
|
+
* If false, missing tenantId is silently ignored (not recommended for production)
|
|
30
|
+
*
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
strict?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Enable bypass capability - allows queries to bypass tenant filter
|
|
36
|
+
* If false, ALL queries must include tenant filter (maximum security)
|
|
37
|
+
*
|
|
38
|
+
* @default true (allows bypassTenant: true for admin operations)
|
|
39
|
+
*/
|
|
40
|
+
allowBypass?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Tenant filter plugin factory
|
|
44
|
+
*
|
|
45
|
+
* Creates a plugin that automatically injects tenant filters into all queries.
|
|
46
|
+
* Prevents cross-tenant data leaks by enforcing tenant isolation at repository level.
|
|
47
|
+
*
|
|
48
|
+
* @param options - Plugin configuration options
|
|
49
|
+
* @returns MongoKit plugin instance
|
|
50
|
+
*
|
|
51
|
+
* @example Multi-Tenant with Strict Mode
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const plugin = tenantFilterPlugin({
|
|
54
|
+
* tenantField: 'context.tenantId',
|
|
55
|
+
* strict: true,
|
|
56
|
+
* allowBypass: false // No bypasses allowed
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example Single-Tenant with Static ID
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const plugin = tenantFilterPlugin({
|
|
63
|
+
* tenantField: 'context.tenantId',
|
|
64
|
+
* staticTenantId: process.env.ORGANIZATION_ID
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function tenantFilterPlugin(options?: TenantFilterOptions): Plugin;
|
|
69
|
+
/**
|
|
70
|
+
* Helper to create single-tenant repository (syntactic sugar)
|
|
71
|
+
*
|
|
72
|
+
* @param tenantId - Static tenant ID for all operations
|
|
73
|
+
* @param tenantField - Field path for tenant ID (default: 'context.tenantId')
|
|
74
|
+
* @returns Plugin configuration for single-tenant mode
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import { singleTenantPlugin } from './plugins/tenant-filter.plugin';
|
|
79
|
+
*
|
|
80
|
+
* const repo = new Repository(Model, [
|
|
81
|
+
* singleTenantPlugin('my-organization-id')
|
|
82
|
+
* ]);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function singleTenantPlugin(tenantId: string, tenantField?: string): Plugin;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/storage/run.repository.d.ts
|
|
88
|
+
type LeanWorkflowRun<TContext = unknown> = WorkflowRun<TContext>;
|
|
89
|
+
interface AtomicUpdateOptions {
|
|
90
|
+
tenantId?: string;
|
|
91
|
+
bypassTenant?: boolean;
|
|
92
|
+
}
|
|
93
|
+
interface PaginatedResult<T> {
|
|
94
|
+
docs: T[];
|
|
95
|
+
page?: number;
|
|
96
|
+
limit?: number;
|
|
97
|
+
total?: number;
|
|
98
|
+
hasMore?: boolean;
|
|
99
|
+
next?: string;
|
|
100
|
+
}
|
|
101
|
+
interface WorkflowRepositoryConfig {
|
|
102
|
+
multiTenant?: TenantFilterOptions;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Repository interface for workflow run storage operations.
|
|
106
|
+
* Provides CRUD, queries, and atomic updates for workflow runs.
|
|
107
|
+
*/
|
|
108
|
+
interface WorkflowRunRepository {
|
|
109
|
+
/** Create a new workflow run */
|
|
110
|
+
create<TContext = unknown>(run: WorkflowRun<TContext>): Promise<WorkflowRun<TContext>>;
|
|
111
|
+
/** Get a workflow run by ID */
|
|
112
|
+
getById<TContext = unknown>(id: string): Promise<WorkflowRun<TContext> | null>;
|
|
113
|
+
/** Get all workflow runs with filtering and pagination */
|
|
114
|
+
getAll: Repository<WorkflowRun>['getAll'];
|
|
115
|
+
/** Update a workflow run */
|
|
116
|
+
update<TContext = unknown>(id: string, data: Partial<WorkflowRun<TContext>>, options?: AtomicUpdateOptions): Promise<WorkflowRun<TContext>>;
|
|
117
|
+
/** Delete a workflow run */
|
|
118
|
+
delete: Repository<WorkflowRun>['delete'];
|
|
119
|
+
/** Atomic update with filter (for concurrent workflow claiming) */
|
|
120
|
+
updateOne(filter: Record<string, unknown>, update: Record<string, unknown>, options?: AtomicUpdateOptions): Promise<{
|
|
121
|
+
modifiedCount: number;
|
|
122
|
+
}>;
|
|
123
|
+
/** Get all active (running/waiting) workflow runs */
|
|
124
|
+
getActiveRuns(): Promise<LeanWorkflowRun[]>;
|
|
125
|
+
/** Get workflow runs by workflow ID */
|
|
126
|
+
getRunsByWorkflow(workflowId: string, limit?: number): Promise<LeanWorkflowRun[]>;
|
|
127
|
+
/** Get all waiting workflow runs */
|
|
128
|
+
getWaitingRuns(): Promise<LeanWorkflowRun[]>;
|
|
129
|
+
/** Get all running workflow runs */
|
|
130
|
+
getRunningRuns(): Promise<LeanWorkflowRun[]>;
|
|
131
|
+
/** Get workflows ready to resume (resumeAt <= now) */
|
|
132
|
+
getReadyToResume(now: Date, limit?: number): Promise<LeanWorkflowRun[]>;
|
|
133
|
+
/** Get workflows ready for retry (retryAt <= now) */
|
|
134
|
+
getReadyForRetry(now: Date, limit?: number): Promise<LeanWorkflowRun[]>;
|
|
135
|
+
/** Get stale running workflows (no heartbeat in threshold) */
|
|
136
|
+
getStaleRunningWorkflows(staleThresholdMs: number, limit?: number): Promise<LeanWorkflowRun[]>;
|
|
137
|
+
/** Get scheduled workflows ready to execute */
|
|
138
|
+
getScheduledWorkflowsReadyToExecute(now: Date, options?: {
|
|
139
|
+
page?: number;
|
|
140
|
+
limit?: number;
|
|
141
|
+
cursor?: string | null;
|
|
142
|
+
tenantId?: string;
|
|
143
|
+
}): Promise<PaginatedResult<LeanWorkflowRun>>;
|
|
144
|
+
/** Access to underlying MongoKit repository */
|
|
145
|
+
readonly base: Repository<WorkflowRun>;
|
|
146
|
+
/** Access to repository hooks */
|
|
147
|
+
readonly _hooks: Map<string, unknown[]>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create a workflow repository instance.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* // Single-tenant:
|
|
154
|
+
* const repo = createWorkflowRepository();
|
|
155
|
+
*
|
|
156
|
+
* // Multi-tenant:
|
|
157
|
+
* const repo = createWorkflowRepository({
|
|
158
|
+
* multiTenant: { tenantField: 'context.tenantId', strict: true }
|
|
159
|
+
* });
|
|
160
|
+
*/
|
|
161
|
+
declare function createWorkflowRepository(config?: WorkflowRepositoryConfig): WorkflowRunRepository;
|
|
162
|
+
declare const workflowRunRepository: WorkflowRunRepository;
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/execution/smart-scheduler.d.ts
|
|
165
|
+
interface SchedulerStats {
|
|
166
|
+
totalPolls: number;
|
|
167
|
+
successfulPolls: number;
|
|
168
|
+
failedPolls: number;
|
|
169
|
+
lastPollAt?: Date;
|
|
170
|
+
avgPollDuration: number;
|
|
171
|
+
activeWorkflows: number;
|
|
172
|
+
resumedWorkflows: number;
|
|
173
|
+
missedResumes: number;
|
|
174
|
+
isPolling: boolean;
|
|
175
|
+
pollInterval: number;
|
|
176
|
+
uptime: number;
|
|
177
|
+
}
|
|
178
|
+
declare class SchedulerMetrics {
|
|
179
|
+
private stats;
|
|
180
|
+
private pollDurations;
|
|
181
|
+
private startTime?;
|
|
182
|
+
private readonly maxDurationsToTrack;
|
|
183
|
+
start(pollInterval: number): void;
|
|
184
|
+
stop(): void;
|
|
185
|
+
recordPoll(duration: number, success: boolean, workflowsFound: number): void;
|
|
186
|
+
recordResume(success: boolean): void;
|
|
187
|
+
getStats(): SchedulerStats;
|
|
188
|
+
isHealthy(): boolean;
|
|
189
|
+
}
|
|
190
|
+
interface SmartSchedulerConfig {
|
|
191
|
+
/** Base poll interval (when active) */
|
|
192
|
+
basePollInterval: number;
|
|
193
|
+
/** Max poll interval (when load is low) */
|
|
194
|
+
maxPollInterval: number;
|
|
195
|
+
/** Min poll interval (when load is high) */
|
|
196
|
+
minPollInterval: number;
|
|
197
|
+
/** Max workflows to process per poll cycle (for efficiency at scale) */
|
|
198
|
+
maxWorkflowsPerPoll: number;
|
|
199
|
+
/** How long to wait before stopping (no workflows) */
|
|
200
|
+
idleTimeout: number;
|
|
201
|
+
/** Circuit breaker: max consecutive failures */
|
|
202
|
+
maxConsecutiveFailures: number;
|
|
203
|
+
/** Enable adaptive polling */
|
|
204
|
+
adaptivePolling: boolean;
|
|
205
|
+
/** Stale workflow check interval (runs even when scheduler is idle) */
|
|
206
|
+
staleCheckInterval: number;
|
|
207
|
+
/** Threshold for stale workflow detection */
|
|
208
|
+
staleThreshold: number;
|
|
209
|
+
}
|
|
210
|
+
declare class SmartScheduler {
|
|
211
|
+
private repository;
|
|
212
|
+
private resumeCallback;
|
|
213
|
+
private config;
|
|
214
|
+
private eventBus?;
|
|
215
|
+
private timers;
|
|
216
|
+
private pollInterval?;
|
|
217
|
+
private idleTimer?;
|
|
218
|
+
private staleCheckTimer?;
|
|
219
|
+
private isPolling;
|
|
220
|
+
private isStaleCheckActive;
|
|
221
|
+
private currentInterval;
|
|
222
|
+
private consecutiveFailures;
|
|
223
|
+
private lastWorkflowCount;
|
|
224
|
+
private metrics;
|
|
225
|
+
private staleRecoveryCallback?;
|
|
226
|
+
private retryCallback?;
|
|
227
|
+
constructor(repository: WorkflowRunRepository, resumeCallback: (runId: string) => Promise<void>, config?: SmartSchedulerConfig, eventBus?: WorkflowEventBus | undefined);
|
|
228
|
+
private emitError;
|
|
229
|
+
/**
|
|
230
|
+
* Set callback for recovering stale 'running' workflows
|
|
231
|
+
* Separate from resume because stale recovery requires different atomic claim logic
|
|
232
|
+
*/
|
|
233
|
+
setStaleRecoveryCallback(callback: (runId: string, thresholdMs: number) => Promise<unknown>): void;
|
|
234
|
+
/**
|
|
235
|
+
* Set callback for retrying failed steps
|
|
236
|
+
* Separate from resume because retries need execute() (re-run step), not resumeStep() (mark done with payload)
|
|
237
|
+
*/
|
|
238
|
+
setRetryCallback(callback: (runId: string) => Promise<unknown>): void;
|
|
239
|
+
/**
|
|
240
|
+
* Intelligent start: Only starts if workflows exist
|
|
241
|
+
* Checks for both waiting workflows AND running workflows that might need stale recovery
|
|
242
|
+
*/
|
|
243
|
+
startIfNeeded(): Promise<boolean>;
|
|
244
|
+
/**
|
|
245
|
+
* Force start polling immediately
|
|
246
|
+
*/
|
|
247
|
+
start(): void;
|
|
248
|
+
/**
|
|
249
|
+
* Stop polling
|
|
250
|
+
*/
|
|
251
|
+
stop(): void;
|
|
252
|
+
/**
|
|
253
|
+
* Schedule a workflow to resume at specific time
|
|
254
|
+
* Handles both short delays (setTimeout) and long delays (MongoDB polling)
|
|
255
|
+
*/
|
|
256
|
+
scheduleResume(runId: string, resumeAt: Date): void;
|
|
257
|
+
/**
|
|
258
|
+
* Cancel scheduled resume
|
|
259
|
+
*/
|
|
260
|
+
cancelSchedule(runId: string): void;
|
|
261
|
+
/**
|
|
262
|
+
* Get scheduler statistics
|
|
263
|
+
*/
|
|
264
|
+
getStats(): ReturnType<SchedulerMetrics['getStats']>;
|
|
265
|
+
/**
|
|
266
|
+
* Health check
|
|
267
|
+
*/
|
|
268
|
+
isHealthy(): boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Get current polling interval
|
|
271
|
+
*/
|
|
272
|
+
getCurrentInterval(): number;
|
|
273
|
+
private startPolling;
|
|
274
|
+
private stopPolling;
|
|
275
|
+
/**
|
|
276
|
+
* Start background stale workflow check
|
|
277
|
+
* Runs independently of main polling loop to ensure crashed workflows are always recovered
|
|
278
|
+
*/
|
|
279
|
+
private startStaleCheck;
|
|
280
|
+
/**
|
|
281
|
+
* Stop background stale workflow check
|
|
282
|
+
* Guarantees no further checks will be scheduled, even if one is currently running
|
|
283
|
+
*/
|
|
284
|
+
private stopStaleCheck;
|
|
285
|
+
/**
|
|
286
|
+
* Background check for stale workflows (runs even when scheduler is idle)
|
|
287
|
+
* If stale workflows found, start the main polling loop
|
|
288
|
+
*/
|
|
289
|
+
private checkForStaleWorkflows;
|
|
290
|
+
private schedulePoll;
|
|
291
|
+
private poll;
|
|
292
|
+
private resumeWorkflow;
|
|
293
|
+
private hasWaitingWorkflows;
|
|
294
|
+
/**
|
|
295
|
+
* Check if there are any active workflows that need scheduler attention
|
|
296
|
+
* Checks for waiting workflows (resume/retry), scheduled workflows, OR stale running workflows
|
|
297
|
+
* Note: Healthy running workflows don't need the scheduler
|
|
298
|
+
*/
|
|
299
|
+
private hasActiveWorkflows;
|
|
300
|
+
private adjustInterval;
|
|
301
|
+
private resetIdleTimer;
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/storage/cache.d.ts
|
|
305
|
+
/** Cache health status for monitoring and alerting */
|
|
306
|
+
type CacheHealthStatus = 'healthy' | 'warning' | 'critical';
|
|
307
|
+
/**
|
|
308
|
+
* O(1) LRU cache using Map's insertion-order iteration
|
|
309
|
+
*
|
|
310
|
+
* Map maintains insertion order, so we use delete+set to move items to the end.
|
|
311
|
+
* - set/get/delete: O(1)
|
|
312
|
+
* - eviction: O(1) - just delete first key
|
|
313
|
+
* - Only caches active workflows (running/waiting)
|
|
314
|
+
*/
|
|
315
|
+
declare class WorkflowCache {
|
|
316
|
+
private readonly maxSize;
|
|
317
|
+
private cache;
|
|
318
|
+
constructor(maxSize?: number);
|
|
319
|
+
set<TContext>(run: WorkflowRun<TContext>): void;
|
|
320
|
+
get<TContext = unknown>(runId: string): WorkflowRun<TContext> | null;
|
|
321
|
+
delete(runId: string): void;
|
|
322
|
+
clear(): void;
|
|
323
|
+
getActive<TContext = unknown>(): WorkflowRun<TContext>[];
|
|
324
|
+
size(): number;
|
|
325
|
+
getStats(): {
|
|
326
|
+
size: number;
|
|
327
|
+
maxSize: number;
|
|
328
|
+
utilizationPercent: number;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Check if cache is approaching capacity.
|
|
332
|
+
* Useful for monitoring and proactive scaling.
|
|
333
|
+
*
|
|
334
|
+
* @param threshold - Utilization ratio (0-1), default 0.8 (80%)
|
|
335
|
+
*/
|
|
336
|
+
isNearCapacity(threshold?: number): boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Get cache health status for monitoring dashboards.
|
|
339
|
+
* Returns status and human-readable message.
|
|
340
|
+
*/
|
|
341
|
+
getHealth(): {
|
|
342
|
+
status: CacheHealthStatus;
|
|
343
|
+
message: string;
|
|
344
|
+
utilizationPercent: number;
|
|
345
|
+
};
|
|
346
|
+
private isActive;
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/core/container.d.ts
|
|
350
|
+
/**
|
|
351
|
+
* Container holding all shared dependencies for a workflow engine instance.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```typescript
|
|
355
|
+
* // Use default container (creates new instances)
|
|
356
|
+
* const container = createContainer();
|
|
357
|
+
*
|
|
358
|
+
* // Use in workflow
|
|
359
|
+
* const workflow = createWorkflow('my-workflow', {
|
|
360
|
+
* steps: { ... },
|
|
361
|
+
* container
|
|
362
|
+
* });
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
interface StreamlineContainer {
|
|
366
|
+
/** MongoDB repository for workflow runs */
|
|
367
|
+
repository: WorkflowRunRepository;
|
|
368
|
+
/** Event bus for workflow lifecycle events */
|
|
369
|
+
eventBus: WorkflowEventBus;
|
|
370
|
+
/** In-memory cache for active workflows */
|
|
371
|
+
cache: WorkflowCache;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Options for creating a container
|
|
375
|
+
*/
|
|
376
|
+
interface ContainerOptions {
|
|
377
|
+
/**
|
|
378
|
+
* Custom repository instance or configuration
|
|
379
|
+
* - If WorkflowRunRepository: uses the provided instance
|
|
380
|
+
* - If WorkflowRepositoryConfig: creates a new repository with the config
|
|
381
|
+
* - If undefined: uses the default singleton repository
|
|
382
|
+
*/
|
|
383
|
+
repository?: WorkflowRunRepository | WorkflowRepositoryConfig;
|
|
384
|
+
/**
|
|
385
|
+
* Custom event bus instance
|
|
386
|
+
* - If WorkflowEventBus: uses the provided instance
|
|
387
|
+
* - If 'global': uses the globalEventBus (for telemetry integration)
|
|
388
|
+
* - If undefined: creates a new isolated event bus
|
|
389
|
+
*/
|
|
390
|
+
eventBus?: WorkflowEventBus | 'global';
|
|
391
|
+
/**
|
|
392
|
+
* Custom cache instance
|
|
393
|
+
* If undefined: creates a new isolated cache
|
|
394
|
+
*/
|
|
395
|
+
cache?: WorkflowCache;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Create a new container with configurable dependencies.
|
|
399
|
+
*
|
|
400
|
+
* @param options - Optional configuration for container dependencies
|
|
401
|
+
* @returns A container with the specified or default dependencies
|
|
402
|
+
*
|
|
403
|
+
* @example Default container (isolated instances)
|
|
404
|
+
* ```typescript
|
|
405
|
+
* const container = createContainer();
|
|
406
|
+
* ```
|
|
407
|
+
*
|
|
408
|
+
* @example Multi-tenant container
|
|
409
|
+
* ```typescript
|
|
410
|
+
* const container = createContainer({
|
|
411
|
+
* repository: { multiTenant: { tenantField: 'meta.tenantId', strict: true } }
|
|
412
|
+
* });
|
|
413
|
+
* ```
|
|
414
|
+
*
|
|
415
|
+
* @example Container with global event bus (for telemetry)
|
|
416
|
+
* ```typescript
|
|
417
|
+
* const container = createContainer({ eventBus: 'global' });
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* @example Fully custom container
|
|
421
|
+
* ```typescript
|
|
422
|
+
* const container = createContainer({
|
|
423
|
+
* repository: myCustomRepo,
|
|
424
|
+
* eventBus: myCustomEventBus,
|
|
425
|
+
* cache: myCustomCache
|
|
426
|
+
* });
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
declare function createContainer(options?: ContainerOptions): StreamlineContainer;
|
|
430
|
+
/**
|
|
431
|
+
* Type guard to check if an object is a valid StreamlineContainer
|
|
432
|
+
*/
|
|
433
|
+
declare function isStreamlineContainer(obj: unknown): obj is StreamlineContainer;
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/execution/engine.d.ts
|
|
436
|
+
/**
|
|
437
|
+
* Registry mapping runId to the engine managing that run.
|
|
438
|
+
* Enables resumeHook() to find the correct engine for resuming.
|
|
439
|
+
*/
|
|
440
|
+
declare class HookRegistry {
|
|
441
|
+
private engines;
|
|
442
|
+
private cleanupInterval;
|
|
443
|
+
register(runId: string, engine: WorkflowEngine<unknown>): void;
|
|
444
|
+
unregister(runId: string): void;
|
|
445
|
+
getEngine(runId: string): WorkflowEngine<unknown> | undefined;
|
|
446
|
+
private cleanup;
|
|
447
|
+
shutdown(): void;
|
|
448
|
+
}
|
|
449
|
+
/** Global hook registry instance */
|
|
450
|
+
declare const hookRegistry: HookRegistry;
|
|
451
|
+
interface WorkflowEngineOptions {
|
|
452
|
+
/** Auto-execute workflow after start (default: true) */
|
|
453
|
+
autoExecute?: boolean;
|
|
454
|
+
/** Custom scheduler configuration */
|
|
455
|
+
scheduler?: Partial<SmartSchedulerConfig>;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Core workflow execution engine.
|
|
459
|
+
*
|
|
460
|
+
* Manages workflow lifecycle: start, execute, pause, resume, cancel.
|
|
461
|
+
* Handles waiting states, retries, and crash recovery automatically.
|
|
462
|
+
*
|
|
463
|
+
* @typeParam TContext - Type of workflow context
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* ```typescript
|
|
467
|
+
* const engine = new WorkflowEngine(definition, handlers, container);
|
|
468
|
+
* const run = await engine.start({ orderId: '123' });
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
declare class WorkflowEngine<TContext = Record<string, unknown>> {
|
|
472
|
+
readonly handlers: WorkflowHandlers<TContext>;
|
|
473
|
+
private executor;
|
|
474
|
+
private scheduler;
|
|
475
|
+
private registry;
|
|
476
|
+
private options;
|
|
477
|
+
private eventListeners;
|
|
478
|
+
/** Exposed for hook registry and external access */
|
|
479
|
+
readonly container: StreamlineContainer;
|
|
480
|
+
constructor(definition: WorkflowDefinition<TContext>, handlers: WorkflowHandlers<TContext>, container: StreamlineContainer, options?: WorkflowEngineOptions);
|
|
481
|
+
/** Get the workflow definition */
|
|
482
|
+
get definition(): WorkflowDefinition<TContext>;
|
|
483
|
+
/**
|
|
484
|
+
* Start a new workflow run.
|
|
485
|
+
*
|
|
486
|
+
* @param input - Input data for the workflow
|
|
487
|
+
* @param meta - Optional metadata (userId, tags, etc.)
|
|
488
|
+
* @returns The created workflow run
|
|
489
|
+
*/
|
|
490
|
+
start(input: unknown, meta?: Record<string, unknown>): Promise<WorkflowRun<TContext>>;
|
|
491
|
+
/**
|
|
492
|
+
* Get a workflow run by ID.
|
|
493
|
+
* Returns from cache if available, otherwise fetches from database.
|
|
494
|
+
*
|
|
495
|
+
* @param runId - Workflow run ID
|
|
496
|
+
* @returns The workflow run or null if not found
|
|
497
|
+
*/
|
|
498
|
+
get(runId: string): Promise<WorkflowRun<TContext> | null>;
|
|
499
|
+
/**
|
|
500
|
+
* Execute a workflow run to completion.
|
|
501
|
+
*
|
|
502
|
+
* Runs steps sequentially until:
|
|
503
|
+
* - All steps complete (status: 'done')
|
|
504
|
+
* - A step fails after retries (status: 'failed')
|
|
505
|
+
* - A step waits for external input (status: 'waiting')
|
|
506
|
+
* - The workflow is cancelled (status: 'cancelled')
|
|
507
|
+
*
|
|
508
|
+
* @param runId - Workflow run ID to execute
|
|
509
|
+
* @returns The updated workflow run
|
|
510
|
+
*/
|
|
511
|
+
execute(runId: string): Promise<WorkflowRun<TContext>>;
|
|
512
|
+
private getOrThrow;
|
|
513
|
+
private shouldContinueExecution;
|
|
514
|
+
private executeNextStep;
|
|
515
|
+
private checkNoProgress;
|
|
516
|
+
/**
|
|
517
|
+
* Handle different waiting states (event, retry, timer, human input)
|
|
518
|
+
* @returns true if execution should continue, false to break
|
|
519
|
+
*/
|
|
520
|
+
private handleWaitingState;
|
|
521
|
+
private findCurrentStep;
|
|
522
|
+
/**
|
|
523
|
+
* Handle data corruption scenario (missing step state)
|
|
524
|
+
*/
|
|
525
|
+
private failCorruption;
|
|
526
|
+
/**
|
|
527
|
+
* Register event listener for event-based waits
|
|
528
|
+
* IMPORTANT: Paused workflows will NOT be resumed by events until explicitly resumed by user
|
|
529
|
+
*/
|
|
530
|
+
private handleEventWait;
|
|
531
|
+
/**
|
|
532
|
+
* Handle retry backoff wait with inline execution for short delays
|
|
533
|
+
* @returns true if workflow should continue execution immediately
|
|
534
|
+
*/
|
|
535
|
+
private handleRetryWait;
|
|
536
|
+
/**
|
|
537
|
+
* Handle timer-based wait (sleep) with inline execution for short delays
|
|
538
|
+
* @returns true if workflow should continue execution immediately
|
|
539
|
+
*/
|
|
540
|
+
private handleTimerWait;
|
|
541
|
+
/**
|
|
542
|
+
* Resume a paused or waiting workflow.
|
|
543
|
+
*
|
|
544
|
+
* For waiting workflows:
|
|
545
|
+
* - If waiting for human input: payload is passed as the step output
|
|
546
|
+
* - If waiting for timer/retry: continues execution from current step
|
|
547
|
+
*
|
|
548
|
+
* @param runId - Workflow run ID to resume
|
|
549
|
+
* @param payload - Data to pass to the waiting step (becomes step output)
|
|
550
|
+
* @returns The updated workflow run
|
|
551
|
+
* @throws {InvalidStateError} If workflow is not in waiting/running state
|
|
552
|
+
*/
|
|
553
|
+
resume(runId: string, payload?: unknown): Promise<WorkflowRun<TContext>>;
|
|
554
|
+
private resumeWaitingWorkflow;
|
|
555
|
+
/**
|
|
556
|
+
* Recover a stale 'running' workflow (crashed mid-execution)
|
|
557
|
+
* Uses atomic claim to prevent multiple servers from recovering the same workflow
|
|
558
|
+
*/
|
|
559
|
+
recoverStale(runId: string, staleThresholdMs: number): Promise<WorkflowRun<TContext> | null>;
|
|
560
|
+
/**
|
|
561
|
+
* Execute a retry for a workflow that failed and is waiting for backoff timer
|
|
562
|
+
* Uses atomic claim to prevent multiple servers from retrying the same workflow
|
|
563
|
+
*/
|
|
564
|
+
executeRetry(runId: string): Promise<WorkflowRun<TContext> | null>;
|
|
565
|
+
/**
|
|
566
|
+
* Rewind a workflow to a previous step.
|
|
567
|
+
* Resets all steps from target step onwards to pending state.
|
|
568
|
+
*
|
|
569
|
+
* @param runId - Workflow run ID
|
|
570
|
+
* @param stepId - Step ID to rewind to
|
|
571
|
+
* @returns The rewound workflow run
|
|
572
|
+
*/
|
|
573
|
+
rewindTo(runId: string, stepId: string): Promise<WorkflowRun<TContext>>;
|
|
574
|
+
/**
|
|
575
|
+
* Cancel a running workflow.
|
|
576
|
+
* Cleans up all resources and marks workflow as cancelled.
|
|
577
|
+
*
|
|
578
|
+
* @param runId - Workflow run ID to cancel
|
|
579
|
+
* @returns The cancelled workflow run
|
|
580
|
+
*/
|
|
581
|
+
cancel(runId: string): Promise<WorkflowRun<TContext>>;
|
|
582
|
+
/**
|
|
583
|
+
* Pause a workflow run.
|
|
584
|
+
*
|
|
585
|
+
* Sets the `paused` flag to prevent the scheduler from processing this workflow.
|
|
586
|
+
* Paused workflows can be resumed later with `resume()`.
|
|
587
|
+
*
|
|
588
|
+
* @param runId - Workflow run ID to pause
|
|
589
|
+
* @returns The paused workflow run
|
|
590
|
+
*/
|
|
591
|
+
pause(runId: string): Promise<WorkflowRun<TContext>>;
|
|
592
|
+
/**
|
|
593
|
+
* Configure engine options (scheduler settings, etc.)
|
|
594
|
+
*/
|
|
595
|
+
configure(options: {
|
|
596
|
+
scheduler?: Partial<SmartSchedulerConfig>;
|
|
597
|
+
}): void;
|
|
598
|
+
shutdown(): void;
|
|
599
|
+
/**
|
|
600
|
+
* Get scheduler statistics for monitoring
|
|
601
|
+
*/
|
|
602
|
+
getSchedulerStats(): ReturnType<SmartScheduler['getStats']>;
|
|
603
|
+
/**
|
|
604
|
+
* Check if scheduler is healthy
|
|
605
|
+
*/
|
|
606
|
+
isSchedulerHealthy(): boolean;
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/workflow/define.d.ts
|
|
610
|
+
interface WorkflowConfig<TContext, TInput = unknown> {
|
|
611
|
+
steps: Record<string, StepHandler<unknown, TContext>>;
|
|
612
|
+
context?: (input: TInput) => TContext;
|
|
613
|
+
version?: string;
|
|
614
|
+
defaults?: {
|
|
615
|
+
retries?: number;
|
|
616
|
+
timeout?: number;
|
|
617
|
+
};
|
|
618
|
+
autoExecute?: boolean;
|
|
619
|
+
/** Optional custom container for dependency injection */
|
|
620
|
+
container?: StreamlineContainer;
|
|
621
|
+
}
|
|
622
|
+
/** Options for waitFor method */
|
|
623
|
+
interface WaitForOptions {
|
|
624
|
+
/** Poll interval in ms (default: 1000) */
|
|
625
|
+
pollInterval?: number;
|
|
626
|
+
/** Maximum time to wait in ms (default: no timeout) */
|
|
627
|
+
timeout?: number;
|
|
628
|
+
}
|
|
629
|
+
interface Workflow<TContext, TInput = unknown> {
|
|
630
|
+
start: (input: TInput, meta?: Record<string, unknown>) => Promise<WorkflowRun<TContext>>;
|
|
631
|
+
get: (runId: string) => Promise<WorkflowRun<TContext> | null>;
|
|
632
|
+
execute: (runId: string) => Promise<WorkflowRun<TContext>>;
|
|
633
|
+
resume: (runId: string, payload?: unknown) => Promise<WorkflowRun<TContext>>;
|
|
634
|
+
cancel: (runId: string) => Promise<WorkflowRun<TContext>>;
|
|
635
|
+
pause: (runId: string) => Promise<WorkflowRun<TContext>>;
|
|
636
|
+
rewindTo: (runId: string, stepId: string) => Promise<WorkflowRun<TContext>>;
|
|
637
|
+
/**
|
|
638
|
+
* Wait for a workflow to complete (reach a terminal state).
|
|
639
|
+
* Polls the workflow status until it's done, failed, or cancelled.
|
|
640
|
+
*
|
|
641
|
+
* @param runId - Workflow run ID to wait for
|
|
642
|
+
* @param options - Poll interval and timeout settings
|
|
643
|
+
* @returns The completed workflow run
|
|
644
|
+
* @throws {Error} If timeout is exceeded or workflow not found
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```typescript
|
|
648
|
+
* const run = await workflow.start({ data: 'test' });
|
|
649
|
+
* const completed = await workflow.waitFor(run._id);
|
|
650
|
+
* console.log(completed.status); // 'done' | 'failed' | 'cancelled'
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
waitFor: (runId: string, options?: WaitForOptions) => Promise<WorkflowRun<TContext>>;
|
|
654
|
+
shutdown: () => void;
|
|
655
|
+
definition: WorkflowDefinition<TContext>;
|
|
656
|
+
engine: WorkflowEngine<TContext>;
|
|
657
|
+
/** The container used by this workflow (for testing or custom integrations) */
|
|
658
|
+
container: StreamlineContainer;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Create a workflow with inline step handlers
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```typescript
|
|
665
|
+
* const orderProcess = createWorkflow('order-process', {
|
|
666
|
+
* steps: {
|
|
667
|
+
* validate: async (ctx) => validateOrder(ctx.input),
|
|
668
|
+
* charge: async (ctx) => chargeCard(ctx.getOutput('validate')),
|
|
669
|
+
* fulfill: async (ctx) => shipOrder(ctx.getOutput('charge')),
|
|
670
|
+
* notify: async (ctx) => sendConfirmation(ctx.context.email),
|
|
671
|
+
* },
|
|
672
|
+
* context: (input) => ({ orderId: input.id, email: input.email }),
|
|
673
|
+
* });
|
|
674
|
+
*
|
|
675
|
+
* await orderProcess.start({ id: '123', email: 'user@example.com' });
|
|
676
|
+
* ```
|
|
677
|
+
*
|
|
678
|
+
* @example Custom container for testing
|
|
679
|
+
* ```typescript
|
|
680
|
+
* const container = createContainer();
|
|
681
|
+
* const workflow = createWorkflow('test-workflow', {
|
|
682
|
+
* steps: { ... },
|
|
683
|
+
* container
|
|
684
|
+
* });
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
declare function createWorkflow<TContext = Record<string, unknown>, TInput = unknown>(id: string, config: WorkflowConfig<TContext, TInput>): Workflow<TContext, TInput>;
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/core/status.d.ts
|
|
690
|
+
declare const STEP_STATUS_VALUES: StepStatus[];
|
|
691
|
+
declare const RUN_STATUS_VALUES: RunStatus[];
|
|
692
|
+
declare function isStepStatus(value: unknown): value is StepStatus;
|
|
693
|
+
declare function isRunStatus(value: unknown): value is RunStatus;
|
|
694
|
+
declare function deriveRunStatus<TContext>(run: WorkflowRun<TContext>): RunStatus;
|
|
695
|
+
declare function isValidStepTransition(from: StepStatus, to: StepStatus): boolean;
|
|
696
|
+
declare function isValidRunTransition(from: RunStatus, to: RunStatus): boolean;
|
|
697
|
+
/**
|
|
698
|
+
* Check if a workflow status represents a terminal (final) state
|
|
699
|
+
*/
|
|
700
|
+
declare function isTerminalState(status: RunStatus): boolean;
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/features/hooks.d.ts
|
|
703
|
+
interface HookOptions {
|
|
704
|
+
/** Custom token (default: auto-generated with crypto-random suffix) */
|
|
705
|
+
token?: string;
|
|
706
|
+
}
|
|
707
|
+
interface HookResult {
|
|
708
|
+
/** Token to use for resuming (includes secure random component) */
|
|
709
|
+
token: string;
|
|
710
|
+
/** URL path for webhook (if using webhook manager) */
|
|
711
|
+
path: string;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Create a hook that pauses workflow until external input.
|
|
715
|
+
* The token includes a crypto-random suffix for security.
|
|
716
|
+
*
|
|
717
|
+
* IMPORTANT: Pass the returned token to ctx.wait() to enable token validation:
|
|
718
|
+
* ```typescript
|
|
719
|
+
* const hook = createHook(ctx, 'approval');
|
|
720
|
+
* return ctx.wait(hook.token, { hookToken: hook.token }); // Token stored for validation
|
|
721
|
+
* ```
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```typescript
|
|
725
|
+
* // In step handler
|
|
726
|
+
* async function waitForApproval(ctx) {
|
|
727
|
+
* const hook = createHook(ctx, 'waiting-for-approval');
|
|
728
|
+
* console.log('Resume with token:', hook.token);
|
|
729
|
+
* return ctx.wait(hook.token, { hookToken: hook.token }); // Token validated on resume
|
|
730
|
+
* }
|
|
731
|
+
*
|
|
732
|
+
* // From API route
|
|
733
|
+
* await resumeHook('token-123', { approved: true });
|
|
734
|
+
* ```
|
|
735
|
+
*/
|
|
736
|
+
declare function createHook(ctx: StepContext, reason: string, options?: HookOptions): HookResult;
|
|
737
|
+
/**
|
|
738
|
+
* Resume a paused workflow by hook token.
|
|
739
|
+
*
|
|
740
|
+
* Security: If the workflow was paused with a hookToken in waitingFor.data,
|
|
741
|
+
* this function validates the token before resuming.
|
|
742
|
+
*
|
|
743
|
+
* Multi-worker support: Falls back to DB lookup if engine not in local registry.
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```typescript
|
|
747
|
+
* // API route handler
|
|
748
|
+
* app.post('/hooks/:token', async (req, res) => {
|
|
749
|
+
* const result = await resumeHook(req.params.token, req.body);
|
|
750
|
+
* res.json({ success: true, runId: result.runId, status: result.run.status });
|
|
751
|
+
* });
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
754
|
+
declare function resumeHook(token: string, payload: unknown): Promise<{
|
|
755
|
+
runId: string;
|
|
756
|
+
run: WorkflowRun;
|
|
757
|
+
}>;
|
|
758
|
+
/**
|
|
759
|
+
* Generate a deterministic token for idempotent hooks
|
|
760
|
+
*
|
|
761
|
+
* @example
|
|
762
|
+
* ```typescript
|
|
763
|
+
* // Slack bot - same channel always gets same token
|
|
764
|
+
* const token = hookToken('slack', channelId);
|
|
765
|
+
* const hook = createHook(ctx, 'slack-message', { token });
|
|
766
|
+
* ```
|
|
767
|
+
*/
|
|
768
|
+
declare function hookToken(...parts: string[]): string;
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/execution/context.d.ts
|
|
771
|
+
declare class WaitSignal extends Error {
|
|
772
|
+
type: 'human' | 'webhook' | 'timer' | 'event';
|
|
773
|
+
reason: string;
|
|
774
|
+
data?: unknown | undefined;
|
|
775
|
+
constructor(type: 'human' | 'webhook' | 'timer' | 'event', reason: string, data?: unknown | undefined);
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/storage/run.model.d.ts
|
|
779
|
+
/**
|
|
780
|
+
* MULTI-TENANCY & SCHEDULED WORKFLOWS - COMPOSITE INDEXES
|
|
781
|
+
*
|
|
782
|
+
* For multi-tenant scheduled workflows, add composite indexes with tenantId FIRST.
|
|
783
|
+
* MongoDB can only use indexes if the query matches the prefix pattern.
|
|
784
|
+
*
|
|
785
|
+
* Example: Multi-tenant scheduled workflow polling
|
|
786
|
+
* ```typescript
|
|
787
|
+
* import { WorkflowRunModel } from '@classytic/streamline';
|
|
788
|
+
*
|
|
789
|
+
* // Add composite index: tenantId first, then scheduling fields
|
|
790
|
+
* WorkflowRunModel.collection.createIndex({
|
|
791
|
+
* 'context.tenantId': 1,
|
|
792
|
+
* status: 1,
|
|
793
|
+
* 'scheduling.executionTime': 1,
|
|
794
|
+
* paused: 1
|
|
795
|
+
* });
|
|
796
|
+
*
|
|
797
|
+
* // This query will use the index efficiently:
|
|
798
|
+
* const scheduledWorkflows = await WorkflowRunModel.find({
|
|
799
|
+
* 'context.tenantId': 'tenant123',
|
|
800
|
+
* status: 'draft',
|
|
801
|
+
* 'scheduling.executionTime': { $lte: new Date() },
|
|
802
|
+
* paused: { $ne: true }
|
|
803
|
+
* }).sort({ 'scheduling.executionTime': 1 }).limit(100);
|
|
804
|
+
* ```
|
|
805
|
+
*
|
|
806
|
+
* Example: List workflows by tenant and time
|
|
807
|
+
* ```typescript
|
|
808
|
+
* WorkflowRunModel.collection.createIndex({
|
|
809
|
+
* 'context.tenantId': 1,
|
|
810
|
+
* workflowId: 1,
|
|
811
|
+
* createdAt: -1
|
|
812
|
+
* });
|
|
813
|
+
* ```
|
|
814
|
+
*
|
|
815
|
+
* IMPORTANT: Put tenantId FIRST in all multi-tenant indexes for query efficiency.
|
|
816
|
+
*/
|
|
817
|
+
/**
|
|
818
|
+
* MULTI-TENANCY & CUSTOM INDEXES
|
|
819
|
+
*
|
|
820
|
+
* The engine is unopinionated about multi-tenancy. Add indexes for YOUR needs:
|
|
821
|
+
*
|
|
822
|
+
* Option 1: Add tenantId to metadata and index it
|
|
823
|
+
* ```typescript
|
|
824
|
+
* import { WorkflowRunModel } from '@classytic/streamline';
|
|
825
|
+
*
|
|
826
|
+
* // Add custom index for tenant-scoped queries
|
|
827
|
+
* WorkflowRunModel.collection.createIndex({ 'meta.tenantId': 1, status: 1 });
|
|
828
|
+
* WorkflowRunModel.collection.createIndex({ 'meta.orgId': 1, createdAt: -1 });
|
|
829
|
+
* ```
|
|
830
|
+
*
|
|
831
|
+
* Option 2: Extend the schema (before first use)
|
|
832
|
+
* ```typescript
|
|
833
|
+
* import { WorkflowRunModel } from '@classytic/streamline';
|
|
834
|
+
*
|
|
835
|
+
* WorkflowRunModel.schema.add({
|
|
836
|
+
* tenantId: { type: String, index: true }
|
|
837
|
+
* });
|
|
838
|
+
* WorkflowRunModel.schema.index({ tenantId: 1, status: 1 });
|
|
839
|
+
* ```
|
|
840
|
+
*
|
|
841
|
+
* Then query: engine.get(runId) and filter by tenantId in your app layer
|
|
842
|
+
*/
|
|
843
|
+
/**
|
|
844
|
+
* Export WorkflowRunModel with hot-reload safety
|
|
845
|
+
*
|
|
846
|
+
* The pattern checks if the model already exists before creating a new one.
|
|
847
|
+
* This prevents "OverwriteModelError" in development with hot module replacement.
|
|
848
|
+
*/
|
|
849
|
+
declare let WorkflowRunModel: mongoose.Model<WorkflowRun>;
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/storage/query-builder.d.ts
|
|
852
|
+
declare const RUN_STATUS: {
|
|
853
|
+
readonly DRAFT: "draft";
|
|
854
|
+
readonly RUNNING: "running";
|
|
855
|
+
readonly WAITING: "waiting";
|
|
856
|
+
readonly DONE: "done";
|
|
857
|
+
readonly FAILED: "failed";
|
|
858
|
+
readonly CANCELLED: "cancelled";
|
|
859
|
+
};
|
|
860
|
+
declare const STEP_STATUS: {
|
|
861
|
+
readonly PENDING: "pending";
|
|
862
|
+
readonly RUNNING: "running";
|
|
863
|
+
readonly WAITING: "waiting";
|
|
864
|
+
readonly DONE: "done";
|
|
865
|
+
readonly FAILED: "failed";
|
|
866
|
+
readonly SKIPPED: "skipped";
|
|
867
|
+
};
|
|
868
|
+
declare class WorkflowQueryBuilder {
|
|
869
|
+
private query;
|
|
870
|
+
static create(): WorkflowQueryBuilder;
|
|
871
|
+
withStatus(status: RunStatus | RunStatus[]): this;
|
|
872
|
+
notPaused(): this;
|
|
873
|
+
isPaused(): this;
|
|
874
|
+
withWorkflowId(workflowId: string): this;
|
|
875
|
+
withRunId(runId: string): this;
|
|
876
|
+
withUserId(userId: string): this;
|
|
877
|
+
withTags(tags: string | string[]): this;
|
|
878
|
+
withStepReady(stepStatus: StepStatus, field: string, beforeTime: Date): this;
|
|
879
|
+
withRetryReady(now?: Date): this;
|
|
880
|
+
withTimerReady(now?: Date): this;
|
|
881
|
+
withStaleHeartbeat(thresholdMs: number): this;
|
|
882
|
+
withScheduledBefore(time: Date): this;
|
|
883
|
+
withScheduledAfter(time: Date): this;
|
|
884
|
+
createdBefore(date: Date): this;
|
|
885
|
+
createdAfter(date: Date): this;
|
|
886
|
+
where(conditions: Record<string, unknown>): this;
|
|
887
|
+
build(): FilterQuery;
|
|
888
|
+
}
|
|
889
|
+
declare const CommonQueries: {
|
|
890
|
+
active: () => FilterQuery;
|
|
891
|
+
readyForRetry: (now?: Date) => FilterQuery;
|
|
892
|
+
readyToResume: (now?: Date) => FilterQuery;
|
|
893
|
+
staleRunning: (thresholdMs: number) => FilterQuery;
|
|
894
|
+
scheduledReady: (now?: Date) => FilterQuery;
|
|
895
|
+
byUser: (userId: string, status?: RunStatus | RunStatus[]) => FilterQuery;
|
|
896
|
+
failed: () => FilterQuery;
|
|
897
|
+
completed: () => FilterQuery;
|
|
898
|
+
};
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/storage/definition.model.d.ts
|
|
901
|
+
interface WorkflowDefinitionDoc {
|
|
902
|
+
_id: string;
|
|
903
|
+
workflowId: string;
|
|
904
|
+
name: string;
|
|
905
|
+
description?: string;
|
|
906
|
+
version: string;
|
|
907
|
+
versionMajor: number;
|
|
908
|
+
versionMinor: number;
|
|
909
|
+
versionPatch: number;
|
|
910
|
+
steps: Array<{
|
|
911
|
+
id: string;
|
|
912
|
+
name: string;
|
|
913
|
+
retries?: number;
|
|
914
|
+
timeout?: number;
|
|
915
|
+
condition?: string;
|
|
916
|
+
}>;
|
|
917
|
+
defaults?: {
|
|
918
|
+
retries?: number;
|
|
919
|
+
timeout?: number;
|
|
920
|
+
};
|
|
921
|
+
createdBy?: string;
|
|
922
|
+
updatedBy?: string;
|
|
923
|
+
createdAt: Date;
|
|
924
|
+
updatedAt: Date;
|
|
925
|
+
isActive: boolean;
|
|
926
|
+
tags?: string[];
|
|
927
|
+
metadata?: Record<string, unknown>;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* MULTI-TENANCY & CUSTOM INDEXES
|
|
931
|
+
*
|
|
932
|
+
* This model is intentionally unopinionated about multi-tenancy.
|
|
933
|
+
* Different apps have different needs (tenantId, orgId, workspaceId, etc.)
|
|
934
|
+
*
|
|
935
|
+
* To add custom indexes for your app:
|
|
936
|
+
*
|
|
937
|
+
* import { WorkflowDefinitionModel } from '@classytic/streamline';
|
|
938
|
+
*
|
|
939
|
+
* // Add your custom indexes
|
|
940
|
+
* WorkflowDefinitionModel.collection.createIndex({ tenantId: 1, isActive: 1 });
|
|
941
|
+
* WorkflowDefinitionModel.collection.createIndex({ orgId: 1, createdAt: -1 });
|
|
942
|
+
*
|
|
943
|
+
* OR extend the schema:
|
|
944
|
+
*
|
|
945
|
+
* import { WorkflowDefinitionModel } from '@classytic/streamline';
|
|
946
|
+
* WorkflowDefinitionModel.schema.add({ tenantId: String });
|
|
947
|
+
* WorkflowDefinitionModel.schema.index({ tenantId: 1, isActive: 1 });
|
|
948
|
+
*/
|
|
949
|
+
/**
|
|
950
|
+
* Export WorkflowDefinitionModel with hot-reload safety
|
|
951
|
+
*
|
|
952
|
+
* The pattern checks if the model already exists before creating a new one.
|
|
953
|
+
* This prevents "OverwriteModelError" in development with hot module replacement.
|
|
954
|
+
*/
|
|
955
|
+
declare let WorkflowDefinitionModel: mongoose.Model<WorkflowDefinitionDoc>;
|
|
956
|
+
/**
|
|
957
|
+
* Repository for WorkflowDefinition
|
|
958
|
+
* Optional: Use if you want to store workflows in MongoDB
|
|
959
|
+
*/
|
|
960
|
+
declare const workflowDefinitionRepository: {
|
|
961
|
+
/**
|
|
962
|
+
* Create a new workflow definition (or version)
|
|
963
|
+
*/
|
|
964
|
+
create(definition: Partial<WorkflowDefinitionDoc>): Promise<WorkflowDefinitionDoc>;
|
|
965
|
+
/**
|
|
966
|
+
* Get latest active version of a workflow
|
|
967
|
+
*/
|
|
968
|
+
getLatestVersion(workflowId: string): Promise<WorkflowDefinitionDoc | null>;
|
|
969
|
+
/**
|
|
970
|
+
* Get specific version of a workflow
|
|
971
|
+
*/
|
|
972
|
+
getByVersion(workflowId: string, version: string): Promise<WorkflowDefinitionDoc | null>;
|
|
973
|
+
/**
|
|
974
|
+
* Get all active workflow definitions (latest versions only)
|
|
975
|
+
*/
|
|
976
|
+
getActiveDefinitions(): Promise<WorkflowDefinitionDoc[]>;
|
|
977
|
+
/**
|
|
978
|
+
* Get all versions of a specific workflow
|
|
979
|
+
*/
|
|
980
|
+
getVersionHistory(workflowId: string): Promise<WorkflowDefinitionDoc[]>;
|
|
981
|
+
/**
|
|
982
|
+
* Update a specific workflow version (rare - usually create new version)
|
|
983
|
+
*/
|
|
984
|
+
update(workflowId: string, version: string, updates: Partial<WorkflowDefinitionDoc>): Promise<WorkflowDefinitionDoc | null>;
|
|
985
|
+
/**
|
|
986
|
+
* Deactivate a specific version (or all versions if version not provided)
|
|
987
|
+
*/
|
|
988
|
+
deactivate(workflowId: string, version?: string): Promise<void>;
|
|
989
|
+
/**
|
|
990
|
+
* Deactivate all old versions (keep only latest)
|
|
991
|
+
*/
|
|
992
|
+
deactivateOldVersions(workflowId: string, keepVersion: string): Promise<void>;
|
|
993
|
+
};
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/utils/visualization.d.ts
|
|
996
|
+
interface StepTimeline {
|
|
997
|
+
id: string;
|
|
998
|
+
status: StepState['status'];
|
|
999
|
+
duration: number | null;
|
|
1000
|
+
startedAt?: Date;
|
|
1001
|
+
endedAt?: Date;
|
|
1002
|
+
}
|
|
1003
|
+
interface WorkflowProgress {
|
|
1004
|
+
completed: number;
|
|
1005
|
+
total: number;
|
|
1006
|
+
percentage: number;
|
|
1007
|
+
}
|
|
1008
|
+
interface StepUIState extends StepState {
|
|
1009
|
+
isCurrentStep: boolean;
|
|
1010
|
+
isPastStep: boolean;
|
|
1011
|
+
isFutureStep: boolean;
|
|
1012
|
+
canRewindTo: boolean;
|
|
1013
|
+
}
|
|
1014
|
+
declare function getStepTimeline(run: WorkflowRun): StepTimeline[];
|
|
1015
|
+
declare function getWorkflowProgress(run: WorkflowRun): WorkflowProgress;
|
|
1016
|
+
declare function getStepUIStates(run: WorkflowRun): StepUIState[];
|
|
1017
|
+
declare function getWaitingInfo(run: WorkflowRun): {
|
|
1018
|
+
stepId: string;
|
|
1019
|
+
type: "human" | "webhook" | "timer" | "event";
|
|
1020
|
+
reason: string;
|
|
1021
|
+
resumeAt: Date | undefined;
|
|
1022
|
+
eventName: string | undefined;
|
|
1023
|
+
data: unknown;
|
|
1024
|
+
} | null;
|
|
1025
|
+
declare function canRewindTo(run: WorkflowRun, stepId: string): boolean;
|
|
1026
|
+
declare function getExecutionPath(run: WorkflowRun): string[];
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/config/constants.d.ts
|
|
1029
|
+
/**
|
|
1030
|
+
* Computed values derived from base constants.
|
|
1031
|
+
* Useful for monitoring, alerting, and capacity planning.
|
|
1032
|
+
*/
|
|
1033
|
+
declare const COMPUTED: {
|
|
1034
|
+
/** Cache utilization threshold for warnings (80% of max) */readonly CACHE_WARNING_THRESHOLD: number; /** Cache utilization threshold for critical alerts (95% of max) */
|
|
1035
|
+
readonly CACHE_CRITICAL_THRESHOLD: number; /** JavaScript setTimeout max safe delay (2^31-1 ms = ~24.8 days) */
|
|
1036
|
+
readonly MAX_TIMEOUT_SAFE_MS: 2147483647; /** Retry delay sequence preview (for documentation/debugging) */
|
|
1037
|
+
readonly RETRY_DELAY_SEQUENCE_MS: number[];
|
|
1038
|
+
};
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/scheduling/scheduling.service.d.ts
|
|
1041
|
+
/**
|
|
1042
|
+
* Paginated result type for scheduled workflows
|
|
1043
|
+
*/
|
|
1044
|
+
interface ScheduledWorkflowsResult<TContext = unknown> {
|
|
1045
|
+
docs: WorkflowRun<TContext>[];
|
|
1046
|
+
page?: number;
|
|
1047
|
+
limit?: number;
|
|
1048
|
+
total?: number;
|
|
1049
|
+
hasMore?: boolean;
|
|
1050
|
+
next?: string;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Options for scheduling a workflow
|
|
1054
|
+
*/
|
|
1055
|
+
interface ScheduleWorkflowOptions {
|
|
1056
|
+
/**
|
|
1057
|
+
* Local date/time to execute (in user's timezone, NOT UTC)
|
|
1058
|
+
*
|
|
1059
|
+
* Format: ISO string without timezone - "YYYY-MM-DDTHH:mm:ss"
|
|
1060
|
+
*
|
|
1061
|
+
* @example
|
|
1062
|
+
* ```typescript
|
|
1063
|
+
* scheduledFor: '2024-03-10T09:00:00' // 9:00 AM local time
|
|
1064
|
+
* ```
|
|
1065
|
+
*
|
|
1066
|
+
* This represents the LOCAL time in the target timezone.
|
|
1067
|
+
* The timezone parameter determines which timezone this represents.
|
|
1068
|
+
* Accepts both string and Date (Date will be converted to ISO string using local components).
|
|
1069
|
+
*/
|
|
1070
|
+
scheduledFor: string | Date;
|
|
1071
|
+
/** IANA timezone name (e.g., "America/New_York", "Europe/London") */
|
|
1072
|
+
timezone: string;
|
|
1073
|
+
/** Input data for workflow execution */
|
|
1074
|
+
input: unknown;
|
|
1075
|
+
/** Optional recurrence pattern for repeating workflows */
|
|
1076
|
+
recurrence?: RecurrencePattern;
|
|
1077
|
+
/** Tenant ID for multi-tenant deployments */
|
|
1078
|
+
tenantId?: string;
|
|
1079
|
+
/** User ID who scheduled the workflow */
|
|
1080
|
+
userId?: string;
|
|
1081
|
+
/** Tags for categorization/filtering */
|
|
1082
|
+
tags?: string[];
|
|
1083
|
+
/** Additional metadata */
|
|
1084
|
+
meta?: Record<string, unknown>;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Options for querying scheduled workflows
|
|
1088
|
+
*/
|
|
1089
|
+
interface GetScheduledWorkflowsOptions {
|
|
1090
|
+
/** Page number for offset pagination */
|
|
1091
|
+
page?: number;
|
|
1092
|
+
/** Number of results per page */
|
|
1093
|
+
limit?: number;
|
|
1094
|
+
/** Cursor for keyset pagination (more efficient at scale) */
|
|
1095
|
+
cursor?: string | null;
|
|
1096
|
+
/** Filter by tenant ID */
|
|
1097
|
+
tenantId?: string;
|
|
1098
|
+
/** Filter by specific time range */
|
|
1099
|
+
executionTimeRange?: {
|
|
1100
|
+
from: Date;
|
|
1101
|
+
to: Date;
|
|
1102
|
+
};
|
|
1103
|
+
/** Filter by recurrence pattern */
|
|
1104
|
+
recurring?: boolean;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Scheduling Service configuration
|
|
1108
|
+
*/
|
|
1109
|
+
interface SchedulingServiceConfig {
|
|
1110
|
+
/** Multi-tenant configuration (optional) - shorthand for container.repository config */
|
|
1111
|
+
multiTenant?: TenantFilterOptions;
|
|
1112
|
+
/** Auto-execute workflows when ready (default: true) */
|
|
1113
|
+
autoExecute?: boolean;
|
|
1114
|
+
/**
|
|
1115
|
+
* Custom container or container options
|
|
1116
|
+
* If provided, multiTenant option is ignored (use container.repository instead)
|
|
1117
|
+
*/
|
|
1118
|
+
container?: StreamlineContainer | ContainerOptions;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* SchedulingService - High-level API for timezone-aware workflow scheduling
|
|
1122
|
+
*
|
|
1123
|
+
* Combines WorkflowEngine, TimezoneHandler, and Repository for easy scheduling.
|
|
1124
|
+
* Handles all the complexity of timezone conversion, DST transitions, and scheduling.
|
|
1125
|
+
*
|
|
1126
|
+
* IMPORTANT: Uses a unified container for both scheduling and execution to ensure
|
|
1127
|
+
* consistent multi-tenant isolation and proper hook registration.
|
|
1128
|
+
*
|
|
1129
|
+
* @typeParam TContext - Workflow context type
|
|
1130
|
+
*/
|
|
1131
|
+
declare class SchedulingService<TContext = Record<string, unknown>> {
|
|
1132
|
+
private readonly engine;
|
|
1133
|
+
private readonly workflow;
|
|
1134
|
+
private readonly timezoneHandler;
|
|
1135
|
+
/** Exposed for testing and advanced use cases */
|
|
1136
|
+
readonly container: StreamlineContainer;
|
|
1137
|
+
constructor(workflow: WorkflowDefinition<TContext>, handlers: WorkflowHandlers<TContext>, config?: SchedulingServiceConfig);
|
|
1138
|
+
/** Get the repository (uses container's repository) */
|
|
1139
|
+
private get repository();
|
|
1140
|
+
/**
|
|
1141
|
+
* Convert Date to ISO string format (YYYY-MM-DDTHH:mm:ss)
|
|
1142
|
+
* Uses local components to preserve the "naive" datetime interpretation
|
|
1143
|
+
*/
|
|
1144
|
+
private convertDateToISOString;
|
|
1145
|
+
/**
|
|
1146
|
+
* Schedule a workflow for future execution at a specific timezone
|
|
1147
|
+
*
|
|
1148
|
+
* @param options - Scheduling options with timezone information
|
|
1149
|
+
* @returns Created workflow run with scheduling metadata
|
|
1150
|
+
* @throws {Error} If timezone is invalid or scheduling fails
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```typescript
|
|
1154
|
+
* const run = await service.schedule({
|
|
1155
|
+
* scheduledFor: '2024-06-15T14:30:00',
|
|
1156
|
+
* timezone: 'Europe/London',
|
|
1157
|
+
* input: { userId: '123', action: 'send-reminder' }
|
|
1158
|
+
* });
|
|
1159
|
+
*
|
|
1160
|
+
* // Check if DST transition affected scheduling
|
|
1161
|
+
* if (run.scheduling?.isDSTTransition) {
|
|
1162
|
+
* console.warn('DST Note:', run.scheduling.dstNote);
|
|
1163
|
+
* }
|
|
1164
|
+
* ```
|
|
1165
|
+
*/
|
|
1166
|
+
schedule(options: ScheduleWorkflowOptions): Promise<WorkflowRun<TContext>>;
|
|
1167
|
+
/**
|
|
1168
|
+
* Reschedule an existing workflow to a new time
|
|
1169
|
+
*
|
|
1170
|
+
* @param runId - Workflow run ID to reschedule
|
|
1171
|
+
* @param newScheduledFor - New local date/time as ISO string (format: "YYYY-MM-DDTHH:mm:ss")
|
|
1172
|
+
* @param newTimezone - Optional new timezone (if changing timezone)
|
|
1173
|
+
* @returns Updated workflow run
|
|
1174
|
+
* @throws {Error} If workflow not found or already executed
|
|
1175
|
+
*
|
|
1176
|
+
* @example Reschedule to different time (same timezone)
|
|
1177
|
+
* ```typescript
|
|
1178
|
+
* await service.reschedule(runId, '2024-06-16T15:00:00');
|
|
1179
|
+
* ```
|
|
1180
|
+
*
|
|
1181
|
+
* @example Reschedule to different time AND timezone
|
|
1182
|
+
* ```typescript
|
|
1183
|
+
* await service.reschedule(
|
|
1184
|
+
* runId,
|
|
1185
|
+
* '2024-06-16T10:00:00',
|
|
1186
|
+
* 'America/New_York'
|
|
1187
|
+
* );
|
|
1188
|
+
* ```
|
|
1189
|
+
*/
|
|
1190
|
+
reschedule(runId: string, newScheduledFor: string | Date, newTimezone?: string): Promise<WorkflowRun<TContext>>;
|
|
1191
|
+
/**
|
|
1192
|
+
* Cancel a scheduled workflow
|
|
1193
|
+
*
|
|
1194
|
+
* @param runId - Workflow run ID to cancel
|
|
1195
|
+
* @returns Cancelled workflow run
|
|
1196
|
+
* @throws {Error} If workflow not found or already executed
|
|
1197
|
+
*
|
|
1198
|
+
* @example
|
|
1199
|
+
* ```typescript
|
|
1200
|
+
* await service.cancelScheduled(runId);
|
|
1201
|
+
* ```
|
|
1202
|
+
*/
|
|
1203
|
+
cancelScheduled(runId: string): Promise<WorkflowRun<TContext>>;
|
|
1204
|
+
/**
|
|
1205
|
+
* Get scheduled workflows (with pagination)
|
|
1206
|
+
*
|
|
1207
|
+
* Supports both offset (page-based) and keyset (cursor-based) pagination.
|
|
1208
|
+
* Use keyset pagination for large datasets (>10k workflows) for better performance.
|
|
1209
|
+
*
|
|
1210
|
+
* @param options - Query and pagination options
|
|
1211
|
+
* @returns Paginated results with scheduled workflows
|
|
1212
|
+
*
|
|
1213
|
+
* @example Offset Pagination (Simple)
|
|
1214
|
+
* ```typescript
|
|
1215
|
+
* const result = await service.getScheduled({
|
|
1216
|
+
* page: 1,
|
|
1217
|
+
* limit: 50,
|
|
1218
|
+
* tenantId: 'client-123'
|
|
1219
|
+
* });
|
|
1220
|
+
*
|
|
1221
|
+
* console.log(result.data); // Array of workflows
|
|
1222
|
+
* console.log(result.hasNextPage); // true if more pages exist
|
|
1223
|
+
* ```
|
|
1224
|
+
*
|
|
1225
|
+
* @example Keyset Pagination (Efficient for large datasets)
|
|
1226
|
+
* ```typescript
|
|
1227
|
+
* // First page
|
|
1228
|
+
* const result = await service.getScheduled({
|
|
1229
|
+
* cursor: null,
|
|
1230
|
+
* limit: 1000
|
|
1231
|
+
* });
|
|
1232
|
+
*
|
|
1233
|
+
* // Next page
|
|
1234
|
+
* const nextResult = await service.getScheduled({
|
|
1235
|
+
* cursor: result.nextCursor,
|
|
1236
|
+
* limit: 1000
|
|
1237
|
+
* });
|
|
1238
|
+
* ```
|
|
1239
|
+
*
|
|
1240
|
+
* @example Filter by execution time range
|
|
1241
|
+
* ```typescript
|
|
1242
|
+
* const result = await service.getScheduled({
|
|
1243
|
+
* executionTimeRange: {
|
|
1244
|
+
* from: new Date('2024-06-01'),
|
|
1245
|
+
* to: new Date('2024-06-30')
|
|
1246
|
+
* },
|
|
1247
|
+
* limit: 100
|
|
1248
|
+
* });
|
|
1249
|
+
* ```
|
|
1250
|
+
*/
|
|
1251
|
+
getScheduled(options?: GetScheduledWorkflowsOptions): Promise<ScheduledWorkflowsResult<TContext>>;
|
|
1252
|
+
/**
|
|
1253
|
+
* Get workflow run by ID
|
|
1254
|
+
*
|
|
1255
|
+
* @param runId - Workflow run ID
|
|
1256
|
+
* @returns Workflow run or null if not found
|
|
1257
|
+
*/
|
|
1258
|
+
get(runId: string): Promise<WorkflowRun<TContext> | null>;
|
|
1259
|
+
/**
|
|
1260
|
+
* Execute a scheduled workflow immediately (bypass schedule)
|
|
1261
|
+
*
|
|
1262
|
+
* @param runId - Workflow run ID to execute
|
|
1263
|
+
* @returns Executed workflow run
|
|
1264
|
+
* @throws {Error} If workflow not found or not in draft status
|
|
1265
|
+
*
|
|
1266
|
+
* @example
|
|
1267
|
+
* ```typescript
|
|
1268
|
+
* // Execute a scheduled workflow now instead of waiting for execution time
|
|
1269
|
+
* const run = await service.executeNow(runId);
|
|
1270
|
+
* ```
|
|
1271
|
+
*/
|
|
1272
|
+
executeNow(runId: string): Promise<WorkflowRun<TContext>>;
|
|
1273
|
+
}
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region src/scheduling/timezone-handler.d.ts
|
|
1276
|
+
/**
|
|
1277
|
+
* Result of timezone calculation including DST transition metadata
|
|
1278
|
+
*/
|
|
1279
|
+
interface TimezoneCalculationResult {
|
|
1280
|
+
/** UTC execution time for scheduler to use */
|
|
1281
|
+
executionTime: Date;
|
|
1282
|
+
/** Local time in user's timezone (for display) */
|
|
1283
|
+
localTimeDisplay: string;
|
|
1284
|
+
/** Whether this time falls during a DST transition */
|
|
1285
|
+
isDSTTransition: boolean;
|
|
1286
|
+
/** Human-readable note about DST adjustment (if any) */
|
|
1287
|
+
dstNote?: string;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* TimezoneHandler - Industry-standard timezone conversion with DST edge case handling
|
|
1291
|
+
*
|
|
1292
|
+
* Handles two critical DST edge cases:
|
|
1293
|
+
* 1. Spring Forward (non-existent time): When clocks jump forward, e.g., 2:30 AM doesn't exist
|
|
1294
|
+
* 2. Fall Back (ambiguous time): When clocks fall back, e.g., 1:30 AM occurs twice
|
|
1295
|
+
*
|
|
1296
|
+
* Design Philosophy:
|
|
1297
|
+
* - Store both intent (timezone + local time) AND execution time (UTC)
|
|
1298
|
+
* - Gracefully handle invalid times by adjusting forward
|
|
1299
|
+
* - Warn users about ambiguous times during fall back
|
|
1300
|
+
* - Use IANA timezone database (not abbreviations like "EST")
|
|
1301
|
+
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* ```typescript
|
|
1304
|
+
* const handler = new TimezoneHandler();
|
|
1305
|
+
*
|
|
1306
|
+
* // Schedule for 9:00 AM New York time
|
|
1307
|
+
* const result = handler.calculateExecutionTime(
|
|
1308
|
+
* '2024-03-10T09:00:00',
|
|
1309
|
+
* 'America/New_York'
|
|
1310
|
+
* );
|
|
1311
|
+
*
|
|
1312
|
+
* console.log(result.executionTime); // UTC time for scheduler
|
|
1313
|
+
* console.log(result.isDSTTransition); // false (9 AM is safe)
|
|
1314
|
+
* ```
|
|
1315
|
+
*/
|
|
1316
|
+
declare class TimezoneHandler {
|
|
1317
|
+
/**
|
|
1318
|
+
* Calculate UTC execution time from user's local timezone intent
|
|
1319
|
+
*
|
|
1320
|
+
* @param scheduledFor - Local date/time as ISO string WITHOUT timezone (naive datetime)
|
|
1321
|
+
* Format: "YYYY-MM-DDTHH:mm:ss" (e.g., "2024-03-10T09:00:00")
|
|
1322
|
+
*
|
|
1323
|
+
* This represents the LOCAL time in the target timezone.
|
|
1324
|
+
* Do NOT include timezone offset (Z, +00:00, etc.)
|
|
1325
|
+
*
|
|
1326
|
+
* @param timezone - IANA timezone name (e.g., "America/New_York", "Europe/London")
|
|
1327
|
+
* @returns Calculation result with execution time and DST metadata
|
|
1328
|
+
*
|
|
1329
|
+
* @throws {Error} If timezone is invalid or scheduledFor format is invalid
|
|
1330
|
+
*
|
|
1331
|
+
* @example Basic Usage
|
|
1332
|
+
* ```typescript
|
|
1333
|
+
* // Schedule for 9:00 AM New York time
|
|
1334
|
+
* const result = handler.calculateExecutionTime(
|
|
1335
|
+
* '2024-03-10T09:00:00',
|
|
1336
|
+
* 'America/New_York'
|
|
1337
|
+
* );
|
|
1338
|
+
* console.log(result.executionTime); // UTC Date object for scheduler
|
|
1339
|
+
* console.log(result.localTimeDisplay); // "2024-03-10 09:00:00 EDT"
|
|
1340
|
+
* ```
|
|
1341
|
+
*
|
|
1342
|
+
* @example Spring Forward Edge Case (non-existent time)
|
|
1343
|
+
* ```typescript
|
|
1344
|
+
* const result = handler.calculateExecutionTime(
|
|
1345
|
+
* '2024-03-10T02:30:00', // 2:30 AM doesn't exist (DST springs forward)
|
|
1346
|
+
* 'America/New_York'
|
|
1347
|
+
* );
|
|
1348
|
+
* // Result: Adjusted to 3:30 AM, isDSTTransition=true, dstNote explains adjustment
|
|
1349
|
+
* ```
|
|
1350
|
+
*
|
|
1351
|
+
* @example Fall Back Edge Case (ambiguous time)
|
|
1352
|
+
* ```typescript
|
|
1353
|
+
* const result = handler.calculateExecutionTime(
|
|
1354
|
+
* '2024-11-03T01:30:00', // 1:30 AM occurs twice (DST falls back)
|
|
1355
|
+
* 'America/New_York'
|
|
1356
|
+
* );
|
|
1357
|
+
* // Result: Uses first occurrence (DST), isDSTTransition=true, dstNote warns of ambiguity
|
|
1358
|
+
* ```
|
|
1359
|
+
*/
|
|
1360
|
+
calculateExecutionTime(scheduledFor: Date | string, timezone: string): TimezoneCalculationResult;
|
|
1361
|
+
/**
|
|
1362
|
+
* Validate if a timezone is recognized by IANA database
|
|
1363
|
+
*
|
|
1364
|
+
* @param timezone - Timezone string to validate
|
|
1365
|
+
* @returns true if valid, false otherwise
|
|
1366
|
+
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* ```typescript
|
|
1369
|
+
* handler.isValidTimezone('America/New_York'); // true
|
|
1370
|
+
* handler.isValidTimezone('EST'); // false (use IANA names)
|
|
1371
|
+
* handler.isValidTimezone('Invalid/Zone'); // false
|
|
1372
|
+
* ```
|
|
1373
|
+
*/
|
|
1374
|
+
isValidTimezone(timezone: string): boolean;
|
|
1375
|
+
/**
|
|
1376
|
+
* Get current offset for a timezone (useful for debugging)
|
|
1377
|
+
*
|
|
1378
|
+
* @param timezone - IANA timezone name
|
|
1379
|
+
* @returns Offset in minutes from UTC (e.g., -300 for EST)
|
|
1380
|
+
*
|
|
1381
|
+
* @example
|
|
1382
|
+
* ```typescript
|
|
1383
|
+
* handler.getCurrentOffset('America/New_York'); // -300 (EST) or -240 (EDT)
|
|
1384
|
+
* ```
|
|
1385
|
+
*/
|
|
1386
|
+
getCurrentOffset(timezone: string): number;
|
|
1387
|
+
/**
|
|
1388
|
+
* Check if a timezone is currently in DST
|
|
1389
|
+
*
|
|
1390
|
+
* @param timezone - IANA timezone name
|
|
1391
|
+
* @returns true if currently observing DST, false otherwise
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* ```typescript
|
|
1395
|
+
* handler.isInDST('America/New_York'); // true in summer, false in winter
|
|
1396
|
+
* ```
|
|
1397
|
+
*/
|
|
1398
|
+
isInDST(timezone: string): boolean;
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Singleton instance for convenience
|
|
1402
|
+
* Use this for most cases unless you need custom configuration
|
|
1403
|
+
*/
|
|
1404
|
+
declare const timezoneHandler: TimezoneHandler;
|
|
1405
|
+
//#endregion
|
|
1406
|
+
//#region src/features/parallel.d.ts
|
|
1407
|
+
/**
|
|
1408
|
+
* Parallel Execution Utilities
|
|
1409
|
+
*
|
|
1410
|
+
* Provides utilities for executing multiple async operations in parallel
|
|
1411
|
+
* with support for different modes, concurrency limits, and timeouts.
|
|
1412
|
+
*
|
|
1413
|
+
* @example
|
|
1414
|
+
* ```typescript
|
|
1415
|
+
* // Execute all tasks in parallel
|
|
1416
|
+
* const results = await executeParallel([
|
|
1417
|
+
* () => fetchUser(1),
|
|
1418
|
+
* () => fetchUser(2),
|
|
1419
|
+
* () => fetchUser(3),
|
|
1420
|
+
* ]);
|
|
1421
|
+
*
|
|
1422
|
+
* // With concurrency limit
|
|
1423
|
+
* const results = await executeParallel(tasks, { concurrency: 2 });
|
|
1424
|
+
*
|
|
1425
|
+
* // With allSettled mode (never throws)
|
|
1426
|
+
* const results = await executeParallel(tasks, { mode: 'allSettled' });
|
|
1427
|
+
* ```
|
|
1428
|
+
*/
|
|
1429
|
+
interface ExecuteParallelOptions {
|
|
1430
|
+
/**
|
|
1431
|
+
* Execution mode:
|
|
1432
|
+
* - 'all': Wait for all tasks to complete (throws if any fails)
|
|
1433
|
+
* - 'race': Complete when first task completes
|
|
1434
|
+
* - 'any': Complete when first task succeeds
|
|
1435
|
+
* - 'allSettled': Wait for all tasks, return success/failure for each
|
|
1436
|
+
*
|
|
1437
|
+
* @default 'all'
|
|
1438
|
+
*/
|
|
1439
|
+
mode?: 'all' | 'race' | 'any' | 'allSettled';
|
|
1440
|
+
/**
|
|
1441
|
+
* Maximum number of tasks to run concurrently
|
|
1442
|
+
* @default Infinity
|
|
1443
|
+
*/
|
|
1444
|
+
concurrency?: number;
|
|
1445
|
+
/**
|
|
1446
|
+
* Maximum time in milliseconds for each task
|
|
1447
|
+
* @default undefined (no timeout)
|
|
1448
|
+
*/
|
|
1449
|
+
timeout?: number;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Execute multiple async tasks in parallel with configurable mode,
|
|
1453
|
+
* concurrency limits, and timeouts.
|
|
1454
|
+
*
|
|
1455
|
+
* @param tasks - Array of async task functions to execute
|
|
1456
|
+
* @param options - Execution options (mode, concurrency, timeout)
|
|
1457
|
+
* @returns Array of results (exact type depends on mode)
|
|
1458
|
+
*
|
|
1459
|
+
* @example Basic parallel execution
|
|
1460
|
+
* ```typescript
|
|
1461
|
+
* const results = await executeParallel([
|
|
1462
|
+
* () => fetch('/api/users'),
|
|
1463
|
+
* () => fetch('/api/posts'),
|
|
1464
|
+
* () => fetch('/api/comments'),
|
|
1465
|
+
* ]);
|
|
1466
|
+
* ```
|
|
1467
|
+
*
|
|
1468
|
+
* @example With concurrency limit
|
|
1469
|
+
* ```typescript
|
|
1470
|
+
* // Only 2 requests at a time
|
|
1471
|
+
* const results = await executeParallel(urlTasks, { concurrency: 2 });
|
|
1472
|
+
* ```
|
|
1473
|
+
*
|
|
1474
|
+
* @example With allSettled mode (never throws)
|
|
1475
|
+
* ```typescript
|
|
1476
|
+
* const results = await executeParallel(tasks, { mode: 'allSettled' });
|
|
1477
|
+
* for (const result of results) {
|
|
1478
|
+
* if (result.success) {
|
|
1479
|
+
* console.log('Success:', result.value);
|
|
1480
|
+
* } else {
|
|
1481
|
+
* console.log('Failed:', result.reason);
|
|
1482
|
+
* }
|
|
1483
|
+
* }
|
|
1484
|
+
* ```
|
|
1485
|
+
*/
|
|
1486
|
+
declare function executeParallel<T>(tasks: Array<() => Promise<T>>, options?: ExecuteParallelOptions): Promise<T[] | Array<{
|
|
1487
|
+
success: boolean;
|
|
1488
|
+
value?: T;
|
|
1489
|
+
reason?: unknown;
|
|
1490
|
+
}>>;
|
|
1491
|
+
//#endregion
|
|
1492
|
+
//#region src/features/conditional.d.ts
|
|
1493
|
+
interface ConditionalStep extends Step {
|
|
1494
|
+
condition?: (context: unknown, run: WorkflowRun<unknown>) => boolean | Promise<boolean>;
|
|
1495
|
+
skipIf?: (context: unknown) => boolean | Promise<boolean>;
|
|
1496
|
+
runIf?: (context: unknown) => boolean | Promise<boolean>;
|
|
1497
|
+
}
|
|
1498
|
+
declare function isConditionalStep(step: Step): step is ConditionalStep;
|
|
1499
|
+
declare function shouldSkipStep<TContext>(step: ConditionalStep, context: TContext, run: WorkflowRun<TContext>): Promise<boolean>;
|
|
1500
|
+
declare function createCondition<TContext>(predicate: (context: TContext) => boolean): (context: TContext) => boolean;
|
|
1501
|
+
declare const conditions: {
|
|
1502
|
+
hasValue: <TContext>(key: keyof TContext) => (context: TContext) => boolean;
|
|
1503
|
+
equals: <TContext>(key: keyof TContext, value: TContext[keyof TContext]) => (context: TContext) => boolean;
|
|
1504
|
+
notEquals: <TContext>(key: keyof TContext, value: TContext[keyof TContext]) => (context: TContext) => boolean;
|
|
1505
|
+
greaterThan: <TContext>(key: keyof TContext, value: number) => (context: TContext) => boolean;
|
|
1506
|
+
lessThan: <TContext>(key: keyof TContext, value: number) => (context: TContext) => boolean;
|
|
1507
|
+
in: <TContext>(key: keyof TContext, values: readonly TContext[keyof TContext][]) => (context: TContext) => boolean;
|
|
1508
|
+
and: <TContext>(...predicates: Array<(context: TContext) => boolean>) => (context: TContext) => boolean;
|
|
1509
|
+
or: <TContext>(...predicates: Array<(context: TContext) => boolean>) => (context: TContext) => boolean;
|
|
1510
|
+
not: <TContext>(predicate: (context: TContext) => boolean) => (context: TContext) => boolean;
|
|
1511
|
+
custom: <TContext>(predicate: (context: TContext) => boolean) => (context: TContext) => boolean;
|
|
1512
|
+
};
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region src/utils/errors.d.ts
|
|
1515
|
+
/**
|
|
1516
|
+
* Custom error classes with rich context for better debugging
|
|
1517
|
+
*/
|
|
1518
|
+
/**
|
|
1519
|
+
* Standardized error codes for programmatic error handling.
|
|
1520
|
+
* Use these codes to handle specific error conditions in your application.
|
|
1521
|
+
*
|
|
1522
|
+
* @example
|
|
1523
|
+
* ```typescript
|
|
1524
|
+
* try {
|
|
1525
|
+
* await workflow.resume(runId);
|
|
1526
|
+
* } catch (err) {
|
|
1527
|
+
* if (err.code === ErrorCode.WORKFLOW_NOT_FOUND) {
|
|
1528
|
+
* // Handle missing workflow
|
|
1529
|
+
* }
|
|
1530
|
+
* }
|
|
1531
|
+
* ```
|
|
1532
|
+
*/
|
|
1533
|
+
declare const ErrorCode: {
|
|
1534
|
+
readonly WORKFLOW_NOT_FOUND: "WORKFLOW_NOT_FOUND";
|
|
1535
|
+
readonly WORKFLOW_ALREADY_COMPLETED: "WORKFLOW_ALREADY_COMPLETED";
|
|
1536
|
+
readonly WORKFLOW_CANCELLED: "WORKFLOW_CANCELLED";
|
|
1537
|
+
readonly STEP_NOT_FOUND: "STEP_NOT_FOUND";
|
|
1538
|
+
readonly STEP_TIMEOUT: "STEP_TIMEOUT";
|
|
1539
|
+
readonly STEP_FAILED: "STEP_FAILED";
|
|
1540
|
+
readonly INVALID_STATE: "INVALID_STATE";
|
|
1541
|
+
readonly INVALID_TRANSITION: "INVALID_TRANSITION";
|
|
1542
|
+
readonly DATA_CORRUPTION: "DATA_CORRUPTION";
|
|
1543
|
+
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
1544
|
+
readonly MAX_RETRIES_EXCEEDED: "MAX_RETRIES_EXCEEDED";
|
|
1545
|
+
readonly EXECUTION_ABORTED: "EXECUTION_ABORTED";
|
|
1546
|
+
};
|
|
1547
|
+
type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
1548
|
+
declare class WorkflowError extends Error {
|
|
1549
|
+
readonly context: {
|
|
1550
|
+
runId?: string;
|
|
1551
|
+
workflowId?: string;
|
|
1552
|
+
stepId?: string;
|
|
1553
|
+
[key: string]: unknown;
|
|
1554
|
+
};
|
|
1555
|
+
readonly code: ErrorCode;
|
|
1556
|
+
constructor(message: string, code: ErrorCode, context: {
|
|
1557
|
+
runId?: string;
|
|
1558
|
+
workflowId?: string;
|
|
1559
|
+
stepId?: string;
|
|
1560
|
+
[key: string]: unknown;
|
|
1561
|
+
});
|
|
1562
|
+
toString(): string;
|
|
1563
|
+
}
|
|
1564
|
+
declare class StepNotFoundError extends WorkflowError {
|
|
1565
|
+
constructor(stepId: string, workflowId: string, availableSteps: string[]);
|
|
1566
|
+
}
|
|
1567
|
+
declare class WorkflowNotFoundError extends WorkflowError {
|
|
1568
|
+
constructor(runId: string);
|
|
1569
|
+
}
|
|
1570
|
+
declare class InvalidStateError extends WorkflowError {
|
|
1571
|
+
constructor(action: string, currentState: string, expectedStates: string[], context: {
|
|
1572
|
+
runId?: string;
|
|
1573
|
+
stepId?: string;
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
declare class StepTimeoutError extends WorkflowError {
|
|
1577
|
+
constructor(stepId: string, timeoutMs: number, runId?: string);
|
|
1578
|
+
}
|
|
1579
|
+
declare class DataCorruptionError extends WorkflowError {
|
|
1580
|
+
constructor(reason: string, context: {
|
|
1581
|
+
runId: string;
|
|
1582
|
+
[key: string]: unknown;
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
declare class MaxRetriesExceededError extends WorkflowError {
|
|
1586
|
+
constructor(stepId: string, attempts: number, runId?: string);
|
|
1587
|
+
}
|
|
1588
|
+
//#endregion
|
|
1589
|
+
export { COMPUTED, type CacheHealthStatus, CommonQueries, type ConditionalStep, type ContainerOptions, DataCorruptionError, ErrorCode, type ErrorCode as ErrorCodeType, type ExecuteParallelOptions, type GetScheduledWorkflowsOptions, InferContext, InferHandlersContext, InvalidStateError, MaxRetriesExceededError, RUN_STATUS, RUN_STATUS_VALUES, RecurrencePattern, RunStatus, STEP_STATUS, STEP_STATUS_VALUES, type ScheduleWorkflowOptions, type SchedulerStats, SchedulingInfo, SchedulingService, type SchedulingServiceConfig, type SmartSchedulerConfig, Step, type StepContext, StepError, StepHandler, StepIds, StepNotFoundError, StepState, StepStatus, type StepTimeline, StepTimeoutError, type StepUIState, type StreamlineContainer, type TenantFilterOptions, type TimezoneCalculationResult, TimezoneHandler, TypedHandlers, WaitSignal, WaitingFor, WorkflowCache, WorkflowDefinition, type WorkflowDefinitionDoc, WorkflowDefinitionModel, WorkflowEngine, type WorkflowEngineOptions, WorkflowError, WorkflowEventBus, type WorkflowEventName, WorkflowEventPayload, WorkflowHandlers, WorkflowNotFoundError, type WorkflowProgress, WorkflowQueryBuilder, type WorkflowRepositoryConfig, type WorkflowRun, WorkflowRunModel, type WorkflowRunRepository, 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 };
|