flowfn 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/index.d.mts +1305 -0
  2. package/dist/index.d.ts +1305 -0
  3. package/dist/index.js +3180 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +3088 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/docs/API.md +801 -0
  8. package/docs/USAGE.md +619 -0
  9. package/package.json +75 -0
  10. package/src/adapters/base.ts +46 -0
  11. package/src/adapters/memory.ts +183 -0
  12. package/src/adapters/postgres/index.ts +383 -0
  13. package/src/adapters/postgres/postgres.test.ts +100 -0
  14. package/src/adapters/postgres/schema.ts +110 -0
  15. package/src/adapters/redis.test.ts +124 -0
  16. package/src/adapters/redis.ts +331 -0
  17. package/src/core/flow-fn.test.ts +70 -0
  18. package/src/core/flow-fn.ts +198 -0
  19. package/src/core/metrics.ts +198 -0
  20. package/src/core/scheduler.test.ts +80 -0
  21. package/src/core/scheduler.ts +154 -0
  22. package/src/index.ts +57 -0
  23. package/src/monitoring/health.ts +261 -0
  24. package/src/patterns/backoff.ts +30 -0
  25. package/src/patterns/batching.ts +248 -0
  26. package/src/patterns/circuit-breaker.test.ts +52 -0
  27. package/src/patterns/circuit-breaker.ts +52 -0
  28. package/src/patterns/priority.ts +146 -0
  29. package/src/patterns/rate-limit.ts +290 -0
  30. package/src/patterns/retry.test.ts +62 -0
  31. package/src/queue/batch.test.ts +35 -0
  32. package/src/queue/dependencies.test.ts +33 -0
  33. package/src/queue/dlq.ts +222 -0
  34. package/src/queue/job.ts +67 -0
  35. package/src/queue/queue.ts +243 -0
  36. package/src/queue/types.ts +153 -0
  37. package/src/queue/worker.ts +66 -0
  38. package/src/storage/event-log.ts +205 -0
  39. package/src/storage/job-storage.ts +206 -0
  40. package/src/storage/workflow-storage.ts +182 -0
  41. package/src/stream/stream.ts +194 -0
  42. package/src/stream/types.ts +81 -0
  43. package/src/utils/hashing.ts +29 -0
  44. package/src/utils/id-generator.ts +109 -0
  45. package/src/utils/serialization.ts +142 -0
  46. package/src/utils/time.ts +167 -0
  47. package/src/workflow/advanced.test.ts +43 -0
  48. package/src/workflow/events.test.ts +39 -0
  49. package/src/workflow/types.ts +132 -0
  50. package/src/workflow/workflow.test.ts +55 -0
  51. package/src/workflow/workflow.ts +422 -0
  52. package/tests/dlq.test.ts +205 -0
  53. package/tests/health.test.ts +228 -0
  54. package/tests/integration.test.ts +253 -0
  55. package/tests/stream.test.ts +233 -0
  56. package/tests/workflow.test.ts +286 -0
  57. package/tsconfig.json +17 -0
  58. package/tsup.config.ts +10 -0
  59. package/vitest.config.ts +15 -0
@@ -0,0 +1,194 @@
1
+ import {
2
+ Stream,
3
+ Message,
4
+ PublishOptions,
5
+ SubscribeOptions,
6
+ MessageHandler,
7
+ Subscription,
8
+ ConsumerOptions,
9
+ Consumer,
10
+ StreamInfo,
11
+ TrimStrategy,
12
+ StreamOptions,
13
+ } from "./types.js";
14
+ import { FlowAdapter } from "../adapters/base.js";
15
+ import { v4 as uuidv4 } from "uuid";
16
+
17
+ export class StreamImpl<T = any> implements Stream<T> {
18
+ name: string;
19
+ private adapter: FlowAdapter;
20
+ private options: StreamOptions;
21
+ private messages: Map<string, Message<T>> = new Map();
22
+
23
+ constructor(name: string, adapter: FlowAdapter, options: StreamOptions = {}) {
24
+ this.name = name;
25
+ this.adapter = adapter;
26
+ this.options = options;
27
+ }
28
+
29
+ async publish(data: T, options?: PublishOptions): Promise<string> {
30
+ const message: Message<T> = {
31
+ id: uuidv4(),
32
+ stream: this.name,
33
+ data,
34
+ headers: options?.headers,
35
+ timestamp: Date.now(),
36
+ partition: options?.partition,
37
+ key: options?.key,
38
+ ack: async () => {},
39
+ nack: async () => {},
40
+ };
41
+
42
+ // Store message locally for retrieval
43
+ this.messages.set(message.id, message);
44
+
45
+ // Auto-trim if maxLength is set
46
+ if (this.options.maxLength && this.messages.size > this.options.maxLength) {
47
+ await this.trim({ maxLength: this.options.maxLength });
48
+ }
49
+
50
+ return this.adapter.publish(this.name, message);
51
+ }
52
+
53
+ async publishBatch(
54
+ messages: Array<{ data: T; options?: PublishOptions }>
55
+ ): Promise<string[]> {
56
+ return Promise.all(messages.map((m) => this.publish(m.data, m.options)));
57
+ }
58
+
59
+ async subscribe(
60
+ handler: MessageHandler<T>,
61
+ options?: SubscribeOptions
62
+ ): Promise<Subscription> {
63
+ return this.adapter.subscribe(this.name, handler as any);
64
+ }
65
+
66
+ createConsumer(consumerId: string, options: ConsumerOptions): Consumer<T> {
67
+ let subscription: Subscription | null = null;
68
+ let paused = false;
69
+
70
+ return {
71
+ subscribe: async (handler: MessageHandler<T>) => {
72
+ // If fromBeginning, replay existing messages first
73
+ if (options.fromBeginning && this.messages.size > 0) {
74
+ const sortedMessages = Array.from(this.messages.values()).sort(
75
+ (a, b) => a.timestamp - b.timestamp
76
+ );
77
+
78
+ for (const msg of sortedMessages) {
79
+ if (!paused) {
80
+ await handler(msg).catch(console.error);
81
+ }
82
+ }
83
+ }
84
+
85
+ subscription = await this.adapter.consume(
86
+ this.name,
87
+ options.groupId,
88
+ consumerId,
89
+ handler as any
90
+ );
91
+ },
92
+ pause: async () => {
93
+ paused = true;
94
+ },
95
+ resume: async () => {
96
+ paused = false;
97
+ },
98
+ close: async () => {
99
+ if (subscription) await subscription.unsubscribe();
100
+ },
101
+ };
102
+ }
103
+
104
+ async getInfo(): Promise<StreamInfo> {
105
+ const info = await this.adapter.getStreamInfo(this.name);
106
+ return {
107
+ ...info,
108
+ length: this.messages.size,
109
+ };
110
+ }
111
+
112
+ async trim(strategy: TrimStrategy): Promise<number> {
113
+ const now = Date.now();
114
+ const messagesToDelete: string[] = [];
115
+
116
+ if (strategy.maxLength) {
117
+ // Sort by timestamp and keep only the newest maxLength messages
118
+ const sortedMessages = Array.from(this.messages.entries()).sort(
119
+ (a, b) => b[1].timestamp - a[1].timestamp
120
+ );
121
+
122
+ if (sortedMessages.length > strategy.maxLength) {
123
+ const toRemove = sortedMessages.slice(strategy.maxLength);
124
+ messagesToDelete.push(...toRemove.map(([id]) => id));
125
+ }
126
+ }
127
+
128
+ if (strategy.maxAgeSeconds) {
129
+ const maxAge = strategy.maxAgeSeconds * 1000;
130
+ for (const [id, message] of this.messages.entries()) {
131
+ if (now - message.timestamp > maxAge) {
132
+ messagesToDelete.push(id);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Remove duplicates
138
+ const uniqueToDelete = [...new Set(messagesToDelete)];
139
+ for (const id of uniqueToDelete) {
140
+ this.messages.delete(id);
141
+ }
142
+
143
+ return uniqueToDelete.length;
144
+ }
145
+
146
+ async getMessages(
147
+ start: string,
148
+ end: string,
149
+ count?: number
150
+ ): Promise<Message<T>[]> {
151
+ const allMessages = Array.from(this.messages.values()).sort(
152
+ (a, b) => a.timestamp - b.timestamp
153
+ );
154
+
155
+ // Filter by ID range (lexicographic comparison)
156
+ let filtered = allMessages.filter((m) => m.id >= start && m.id <= end);
157
+
158
+ // Apply count limit if specified
159
+ if (count !== undefined && count > 0) {
160
+ filtered = filtered.slice(0, count);
161
+ }
162
+
163
+ return filtered;
164
+ }
165
+
166
+ /**
167
+ * Replay messages from a specific timestamp
168
+ */
169
+ async replay(
170
+ fromTimestamp: number,
171
+ handler: MessageHandler<T>
172
+ ): Promise<number> {
173
+ const messages = Array.from(this.messages.values())
174
+ .filter((m) => m.timestamp >= fromTimestamp)
175
+ .sort((a, b) => a.timestamp - b.timestamp);
176
+
177
+ for (const message of messages) {
178
+ await handler(message);
179
+ }
180
+
181
+ return messages.length;
182
+ }
183
+
184
+ /**
185
+ * Get message count
186
+ */
187
+ getMessageCount(): number {
188
+ return this.messages.size;
189
+ }
190
+
191
+ async close(): Promise<void> {
192
+ this.messages.clear();
193
+ }
194
+ }
@@ -0,0 +1,81 @@
1
+ export interface StreamOptions {
2
+ maxLength?: number;
3
+ retention?: number;
4
+ partitions?: number;
5
+ }
6
+
7
+ export interface Stream<T = any> {
8
+ name: string;
9
+
10
+ // Publishing
11
+ publish(data: T, options?: PublishOptions): Promise<string>;
12
+ publishBatch(messages: Array<{ data: T; options?: PublishOptions }>): Promise<string[]>;
13
+
14
+ // Subscribing
15
+ subscribe(handler: MessageHandler<T>, options?: SubscribeOptions): Promise<Subscription>;
16
+ createConsumer(consumerId: string, options: ConsumerOptions): Consumer<T>;
17
+
18
+ // Management
19
+ getInfo(): Promise<StreamInfo>;
20
+ trim(strategy: TrimStrategy): Promise<number>;
21
+ getMessages(start: string, end: string, count?: number): Promise<Message<T>[]>;
22
+
23
+ // Lifecycle
24
+ close(): Promise<void>;
25
+ }
26
+
27
+ export interface Message<T = any> {
28
+ id: string;
29
+ stream: string;
30
+ data: T;
31
+ headers?: Record<string, string>;
32
+ timestamp: number;
33
+ partition?: number;
34
+ offset?: number;
35
+ key?: string;
36
+
37
+ ack(): Promise<void>;
38
+ nack(requeue?: boolean): Promise<void>;
39
+ }
40
+
41
+ export interface PublishOptions {
42
+ key?: string;
43
+ headers?: Record<string, string>;
44
+ partition?: number;
45
+ }
46
+
47
+ export interface SubscribeOptions {
48
+ // Basic subscription options if any
49
+ }
50
+
51
+ export interface ConsumerOptions {
52
+ groupId: string;
53
+ fromBeginning?: boolean;
54
+ autoCommit?: boolean;
55
+ commitInterval?: number;
56
+ maxInFlight?: number;
57
+ }
58
+
59
+ export interface Consumer<T = any> {
60
+ subscribe(handler: MessageHandler<T>): Promise<void>;
61
+ pause(): Promise<void>;
62
+ resume(): Promise<void>;
63
+ close(): Promise<void>;
64
+ }
65
+
66
+ export type MessageHandler<T> = (message: Message<T>) => Promise<void>;
67
+
68
+ export interface Subscription {
69
+ unsubscribe(): Promise<void>;
70
+ }
71
+
72
+ export interface StreamInfo {
73
+ name: string;
74
+ length: number;
75
+ groups: number;
76
+ }
77
+
78
+ export interface TrimStrategy {
79
+ maxLength?: number;
80
+ maxAgeSeconds?: number;
81
+ }
@@ -0,0 +1,29 @@
1
+ import { createHash } from "crypto";
2
+
3
+ /**
4
+ * Generate a hash for job deduplication
5
+ */
6
+ export function hashJob(data: any, options?: { algorithm?: string }): string {
7
+ const algorithm = options?.algorithm || "sha256";
8
+ const hash = createHash(algorithm);
9
+
10
+ // Normalize and stringify the data for consistent hashing
11
+ const normalized = JSON.stringify(data, Object.keys(data).sort());
12
+ hash.update(normalized);
13
+
14
+ return hash.digest("hex");
15
+ }
16
+
17
+ /**
18
+ * Generate a deduplication key from job name and data
19
+ */
20
+ export function generateDeduplicationKey(name: string, data: any): string {
21
+ return `${name}:${hashJob(data)}`;
22
+ }
23
+
24
+ /**
25
+ * Check if two job payloads are equivalent
26
+ */
27
+ export function areJobsEquivalent(job1: any, job2: any): boolean {
28
+ return hashJob(job1) === hashJob(job2);
29
+ }
@@ -0,0 +1,109 @@
1
+ import { v4 as uuidv4, v5 as uuidv5, v1 as uuidv1 } from "uuid";
2
+ import { createHash, randomBytes } from "crypto";
3
+
4
+ /**
5
+ * ID generation strategies
6
+ */
7
+ export type IdStrategy =
8
+ | "uuid-v4"
9
+ | "uuid-v5"
10
+ | "uuid-v1"
11
+ | "nanoid"
12
+ | "incremental"
13
+ | "custom";
14
+
15
+ export interface IdGeneratorOptions {
16
+ strategy?: IdStrategy;
17
+ namespace?: string; // For UUID v5
18
+ prefix?: string;
19
+ customGenerator?: () => string;
20
+ }
21
+
22
+ /**
23
+ * Generate a unique identifier based on the specified strategy
24
+ */
25
+ export function generateId(options: IdGeneratorOptions = {}): string {
26
+ const {
27
+ strategy = "uuid-v4",
28
+ namespace,
29
+ prefix = "",
30
+ customGenerator,
31
+ } = options;
32
+
33
+ let id: string;
34
+
35
+ switch (strategy) {
36
+ case "uuid-v4":
37
+ id = uuidv4();
38
+ break;
39
+
40
+ case "uuid-v5":
41
+ if (!namespace) {
42
+ throw new Error("UUID v5 requires a namespace");
43
+ }
44
+ const name = `${Date.now()}-${randomBytes(8).toString("hex")}`;
45
+ id = uuidv5(name, namespace);
46
+ break;
47
+
48
+ case "uuid-v1":
49
+ id = uuidv1();
50
+ break;
51
+
52
+ case "nanoid":
53
+ // Simple nanoid-like implementation
54
+ id = randomBytes(16).toString("base64url");
55
+ break;
56
+
57
+ case "incremental":
58
+ // Simple timestamp-based incremental ID
59
+ id = `${Date.now()}-${randomBytes(4).toString("hex")}`;
60
+ break;
61
+
62
+ case "custom":
63
+ if (!customGenerator) {
64
+ throw new Error("Custom strategy requires customGenerator function");
65
+ }
66
+ id = customGenerator();
67
+ break;
68
+
69
+ default:
70
+ id = uuidv4();
71
+ }
72
+
73
+ return prefix ? `${prefix}${id}` : id;
74
+ }
75
+
76
+ /**
77
+ * Generate a job ID with optional prefix
78
+ */
79
+ export function generateJobId(prefix?: string): string {
80
+ return generateId({ prefix: prefix || "job_" });
81
+ }
82
+
83
+ /**
84
+ * Generate a workflow execution ID
85
+ */
86
+ export function generateExecutionId(prefix?: string): string {
87
+ return generateId({ prefix: prefix || "exec_" });
88
+ }
89
+
90
+ /**
91
+ * Generate a message ID for streams
92
+ */
93
+ export function generateMessageId(prefix?: string): string {
94
+ return generateId({ prefix: prefix || "msg_" });
95
+ }
96
+
97
+ /**
98
+ * Generate a deterministic ID based on content (useful for idempotency)
99
+ */
100
+ export function generateDeterministicId(
101
+ content: string | object,
102
+ prefix?: string
103
+ ): string {
104
+ const hash = createHash("sha256");
105
+ const data = typeof content === "string" ? content : JSON.stringify(content);
106
+ hash.update(data);
107
+ const id = hash.digest("hex").slice(0, 32);
108
+ return prefix ? `${prefix}${id}` : id;
109
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Serialization utilities for FlowFn
3
+ */
4
+
5
+ export interface SerializationOptions {
6
+ pretty?: boolean;
7
+ includeUndefined?: boolean;
8
+ dateFormat?: "iso" | "timestamp";
9
+ }
10
+
11
+ /**
12
+ * Serialize job data to JSON string
13
+ */
14
+ export function serialize<T = any>(
15
+ data: T,
16
+ options: SerializationOptions = {}
17
+ ): string {
18
+ const { pretty = false, dateFormat = "iso" } = options;
19
+
20
+ const replacer = (key: string, value: any) => {
21
+ // Handle dates
22
+ if (value instanceof Date) {
23
+ return dateFormat === "timestamp" ? value.getTime() : value.toISOString();
24
+ }
25
+
26
+ // Handle undefined
27
+ if (value === undefined && !options.includeUndefined) {
28
+ return null;
29
+ }
30
+
31
+ // Handle functions (skip them)
32
+ if (typeof value === "function") {
33
+ return undefined;
34
+ }
35
+
36
+ // Handle BigInt
37
+ if (typeof value === "bigint") {
38
+ return value.toString();
39
+ }
40
+
41
+ return value;
42
+ };
43
+
44
+ return JSON.stringify(data, replacer, pretty ? 2 : undefined);
45
+ }
46
+
47
+ /**
48
+ * Deserialize JSON string to object
49
+ */
50
+ export function deserialize<T = any>(json: string): T {
51
+ return JSON.parse(json, (key, value) => {
52
+ // Try to parse ISO date strings
53
+ if (typeof value === "string") {
54
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
55
+ if (isoDateRegex.test(value)) {
56
+ return new Date(value);
57
+ }
58
+ }
59
+ return value;
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Safely serialize data with circular reference handling
65
+ */
66
+ export function serializeSafe<T = any>(
67
+ data: T,
68
+ options: SerializationOptions = {}
69
+ ): string {
70
+ const seen = new WeakSet();
71
+ const { pretty = false } = options;
72
+
73
+ const replacer = (key: string, value: any) => {
74
+ if (typeof value === "object" && value !== null) {
75
+ if (seen.has(value)) {
76
+ return "[Circular]";
77
+ }
78
+ seen.add(value);
79
+ }
80
+
81
+ // Handle dates
82
+ if (value instanceof Date) {
83
+ return value.toISOString();
84
+ }
85
+
86
+ // Handle functions
87
+ if (typeof value === "function") {
88
+ return "[Function]";
89
+ }
90
+
91
+ // Handle BigInt
92
+ if (typeof value === "bigint") {
93
+ return value.toString();
94
+ }
95
+
96
+ return value;
97
+ };
98
+
99
+ return JSON.stringify(data, replacer, pretty ? 2 : undefined);
100
+ }
101
+
102
+ /**
103
+ * Clone an object using serialization
104
+ */
105
+ export function cloneViaSerialization<T>(obj: T): T {
106
+ return deserialize(serialize(obj));
107
+ }
108
+
109
+ /**
110
+ * Serialize with compression (base64 encode)
111
+ */
112
+ export function serializeCompressed<T = any>(data: T): string {
113
+ const json = serialize(data);
114
+ return Buffer.from(json).toString("base64");
115
+ }
116
+
117
+ /**
118
+ * Deserialize compressed data
119
+ */
120
+ export function deserializeCompressed<T = any>(compressed: string): T {
121
+ const json = Buffer.from(compressed, "base64").toString("utf-8");
122
+ return deserialize(json);
123
+ }
124
+
125
+ /**
126
+ * Check if a value is serializable
127
+ */
128
+ export function isSerializable(value: any): boolean {
129
+ try {
130
+ serialize(value);
131
+ return true;
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get size of serialized data in bytes
139
+ */
140
+ export function getSerializedSize(data: any): number {
141
+ return Buffer.byteLength(serialize(data), "utf-8");
142
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Time utility functions for FlowFn
3
+ */
4
+
5
+ /**
6
+ * Duration units
7
+ */
8
+ export interface Duration {
9
+ milliseconds?: number;
10
+ seconds?: number;
11
+ minutes?: number;
12
+ hours?: number;
13
+ days?: number;
14
+ weeks?: number;
15
+ }
16
+
17
+ /**
18
+ * Convert duration object to milliseconds
19
+ */
20
+ export function toMilliseconds(duration: Duration): number {
21
+ let ms = 0;
22
+
23
+ if (duration.milliseconds) ms += duration.milliseconds;
24
+ if (duration.seconds) ms += duration.seconds * 1000;
25
+ if (duration.minutes) ms += duration.minutes * 60 * 1000;
26
+ if (duration.hours) ms += duration.hours * 60 * 60 * 1000;
27
+ if (duration.days) ms += duration.days * 24 * 60 * 60 * 1000;
28
+ if (duration.weeks) ms += duration.weeks * 7 * 24 * 60 * 60 * 1000;
29
+
30
+ return ms;
31
+ }
32
+
33
+ /**
34
+ * Convert milliseconds to duration object
35
+ */
36
+ export function fromMilliseconds(ms: number): Duration {
37
+ const weeks = Math.floor(ms / (7 * 24 * 60 * 60 * 1000));
38
+ ms %= 7 * 24 * 60 * 60 * 1000;
39
+
40
+ const days = Math.floor(ms / (24 * 60 * 60 * 1000));
41
+ ms %= 24 * 60 * 60 * 1000;
42
+
43
+ const hours = Math.floor(ms / (60 * 60 * 1000));
44
+ ms %= 60 * 60 * 1000;
45
+
46
+ const minutes = Math.floor(ms / (60 * 1000));
47
+ ms %= 60 * 1000;
48
+
49
+ const seconds = Math.floor(ms / 1000);
50
+ const milliseconds = ms % 1000;
51
+
52
+ return { weeks, days, hours, minutes, seconds, milliseconds };
53
+ }
54
+
55
+ /**
56
+ * Format duration as human-readable string
57
+ */
58
+ export function formatDuration(duration: Duration): string {
59
+ const parts: string[] = [];
60
+
61
+ if (duration.weeks) parts.push(`${duration.weeks}w`);
62
+ if (duration.days) parts.push(`${duration.days}d`);
63
+ if (duration.hours) parts.push(`${duration.hours}h`);
64
+ if (duration.minutes) parts.push(`${duration.minutes}m`);
65
+ if (duration.seconds) parts.push(`${duration.seconds}s`);
66
+ if (duration.milliseconds) parts.push(`${duration.milliseconds}ms`);
67
+
68
+ return parts.join(" ") || "0ms";
69
+ }
70
+
71
+ /**
72
+ * Sleep for specified milliseconds
73
+ */
74
+ export function sleep(ms: number): Promise<void> {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }
77
+
78
+ /**
79
+ * Sleep for specified duration
80
+ */
81
+ export async function sleepDuration(duration: Duration): Promise<void> {
82
+ return sleep(toMilliseconds(duration));
83
+ }
84
+
85
+ /**
86
+ * Create a timeout promise
87
+ */
88
+ export function timeout<T>(
89
+ promise: Promise<T>,
90
+ ms: number,
91
+ message?: string
92
+ ): Promise<T> {
93
+ return Promise.race([
94
+ promise,
95
+ new Promise<T>((_, reject) =>
96
+ setTimeout(
97
+ () => reject(new Error(message || `Timeout after ${ms}ms`)),
98
+ ms
99
+ )
100
+ ),
101
+ ]);
102
+ }
103
+
104
+ /**
105
+ * Get current timestamp in milliseconds
106
+ */
107
+ export function now(): number {
108
+ return Date.now();
109
+ }
110
+
111
+ /**
112
+ * Calculate delay until a specific timestamp
113
+ */
114
+ export function delayUntil(timestamp: number): number {
115
+ return Math.max(0, timestamp - Date.now());
116
+ }
117
+
118
+ /**
119
+ * Check if a timestamp is in the past
120
+ */
121
+ export function isPast(timestamp: number): boolean {
122
+ return timestamp < Date.now();
123
+ }
124
+
125
+ /**
126
+ * Check if a timestamp is in the future
127
+ */
128
+ export function isFuture(timestamp: number): boolean {
129
+ return timestamp > Date.now();
130
+ }
131
+
132
+ /**
133
+ * Add duration to timestamp
134
+ */
135
+ export function addDuration(timestamp: number, duration: Duration): number {
136
+ return timestamp + toMilliseconds(duration);
137
+ }
138
+
139
+ /**
140
+ * Parse cron-like duration string (e.g., "5m", "1h", "30s")
141
+ */
142
+ export function parseDuration(str: string): number {
143
+ const match = str.match(/^(\d+)(ms|s|m|h|d|w)$/);
144
+ if (!match) {
145
+ throw new Error(`Invalid duration string: ${str}`);
146
+ }
147
+
148
+ const value = parseInt(match[1], 10);
149
+ const unit = match[2];
150
+
151
+ switch (unit) {
152
+ case "ms":
153
+ return value;
154
+ case "s":
155
+ return value * 1000;
156
+ case "m":
157
+ return value * 60 * 1000;
158
+ case "h":
159
+ return value * 60 * 60 * 1000;
160
+ case "d":
161
+ return value * 24 * 60 * 60 * 1000;
162
+ case "w":
163
+ return value * 7 * 24 * 60 * 60 * 1000;
164
+ default:
165
+ throw new Error(`Unknown unit: ${unit}`);
166
+ }
167
+ }