@onebun/core 0.1.2 → 0.1.3

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 (79) hide show
  1. package/package.json +1 -1
  2. package/src/{application.test.ts → application/application.test.ts} +6 -5
  3. package/src/{application.ts → application/application.ts} +131 -12
  4. package/src/application/index.ts +9 -0
  5. package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
  6. package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
  7. package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
  8. package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
  9. package/src/{decorators.ts → decorators/decorators.ts} +3 -2
  10. package/src/decorators/index.ts +15 -0
  11. package/src/index.ts +47 -134
  12. package/src/module/index.ts +12 -0
  13. package/src/{module.test.ts → module/module.test.ts} +3 -2
  14. package/src/{module.ts → module/module.ts} +6 -5
  15. package/src/queue/adapters/index.ts +8 -0
  16. package/src/queue/adapters/memory.adapter.test.ts +405 -0
  17. package/src/queue/adapters/memory.adapter.ts +509 -0
  18. package/src/queue/adapters/redis.adapter.ts +673 -0
  19. package/src/queue/cron-expression.test.ts +145 -0
  20. package/src/queue/cron-expression.ts +115 -0
  21. package/src/queue/cron-parser.test.ts +185 -0
  22. package/src/queue/cron-parser.ts +287 -0
  23. package/src/queue/decorators.test.ts +292 -0
  24. package/src/queue/decorators.ts +493 -0
  25. package/src/queue/docs-examples.test.ts +449 -0
  26. package/src/queue/guards.test.ts +309 -0
  27. package/src/queue/guards.ts +307 -0
  28. package/src/queue/index.ts +118 -0
  29. package/src/queue/pattern-matcher.test.ts +191 -0
  30. package/src/queue/pattern-matcher.ts +252 -0
  31. package/src/queue/queue.service.ts +421 -0
  32. package/src/queue/scheduler.test.ts +235 -0
  33. package/src/queue/scheduler.ts +379 -0
  34. package/src/queue/types.ts +502 -0
  35. package/src/redis/index.ts +8 -0
  36. package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
  37. package/src/service-client/index.ts +10 -0
  38. package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
  39. package/src/{service-client.ts → service-client/service-client.ts} +1 -1
  40. package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
  41. package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
  42. package/src/testing/index.ts +7 -0
  43. package/src/types.ts +34 -5
  44. package/src/websocket/index.ts +50 -0
  45. package/src/{ws-decorators.ts → websocket/ws-decorators.ts} +2 -1
  46. package/src/{ws-integration.test.ts → websocket/ws-integration.test.ts} +3 -2
  47. package/src/{ws-service-definition.ts → websocket/ws-service-definition.ts} +2 -1
  48. package/src/{ws-storage-redis.ts → websocket/ws-storage-redis.ts} +1 -1
  49. /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
  50. /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
  51. /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
  52. /package/src/{config.service.ts → module/config.service.ts} +0 -0
  53. /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
  54. /package/src/{controller.ts → module/controller.ts} +0 -0
  55. /package/src/{service.test.ts → module/service.test.ts} +0 -0
  56. /package/src/{service.ts → module/service.ts} +0 -0
  57. /package/src/{redis-client.ts → redis/redis-client.ts} +0 -0
  58. /package/src/{shared-redis.ts → redis/shared-redis.ts} +0 -0
  59. /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
  60. /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
  61. /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
  62. /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
  63. /package/src/{ws-base-gateway.test.ts → websocket/ws-base-gateway.test.ts} +0 -0
  64. /package/src/{ws-base-gateway.ts → websocket/ws-base-gateway.ts} +0 -0
  65. /package/src/{ws-client.test.ts → websocket/ws-client.test.ts} +0 -0
  66. /package/src/{ws-client.ts → websocket/ws-client.ts} +0 -0
  67. /package/src/{ws-client.types.ts → websocket/ws-client.types.ts} +0 -0
  68. /package/src/{ws-decorators.test.ts → websocket/ws-decorators.test.ts} +0 -0
  69. /package/src/{ws-guards.test.ts → websocket/ws-guards.test.ts} +0 -0
  70. /package/src/{ws-guards.ts → websocket/ws-guards.ts} +0 -0
  71. /package/src/{ws-handler.ts → websocket/ws-handler.ts} +0 -0
  72. /package/src/{ws-pattern-matcher.test.ts → websocket/ws-pattern-matcher.test.ts} +0 -0
  73. /package/src/{ws-pattern-matcher.ts → websocket/ws-pattern-matcher.ts} +0 -0
  74. /package/src/{ws-socketio-protocol.test.ts → websocket/ws-socketio-protocol.test.ts} +0 -0
  75. /package/src/{ws-socketio-protocol.ts → websocket/ws-socketio-protocol.ts} +0 -0
  76. /package/src/{ws-storage-memory.test.ts → websocket/ws-storage-memory.test.ts} +0 -0
  77. /package/src/{ws-storage-memory.ts → websocket/ws-storage-memory.ts} +0 -0
  78. /package/src/{ws-storage.ts → websocket/ws-storage.ts} +0 -0
  79. /package/src/{ws.types.ts → websocket/ws.types.ts} +0 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Cron Expression Constants Tests
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ } from 'bun:test';
10
+
11
+ import { CronExpression } from './cron-expression';
12
+ import { isValidCronExpression } from './cron-parser';
13
+
14
+ describe('CronExpression', () => {
15
+ describe('seconds patterns', () => {
16
+ it('EVERY_SECOND should be valid', () => {
17
+ expect(isValidCronExpression(CronExpression.EVERY_SECOND)).toBe(true);
18
+ });
19
+
20
+ it('EVERY_5_SECONDS should be valid', () => {
21
+ expect(isValidCronExpression(CronExpression.EVERY_5_SECONDS)).toBe(true);
22
+ });
23
+
24
+ it('EVERY_10_SECONDS should be valid', () => {
25
+ expect(isValidCronExpression(CronExpression.EVERY_10_SECONDS)).toBe(true);
26
+ });
27
+
28
+ it('EVERY_30_SECONDS should be valid', () => {
29
+ expect(isValidCronExpression(CronExpression.EVERY_30_SECONDS)).toBe(true);
30
+ });
31
+ });
32
+
33
+ describe('minutes patterns', () => {
34
+ it('EVERY_MINUTE should be valid', () => {
35
+ expect(isValidCronExpression(CronExpression.EVERY_MINUTE)).toBe(true);
36
+ });
37
+
38
+ it('EVERY_5_MINUTES should be valid', () => {
39
+ expect(isValidCronExpression(CronExpression.EVERY_5_MINUTES)).toBe(true);
40
+ });
41
+
42
+ it('EVERY_10_MINUTES should be valid', () => {
43
+ expect(isValidCronExpression(CronExpression.EVERY_10_MINUTES)).toBe(true);
44
+ });
45
+
46
+ it('EVERY_15_MINUTES should be valid', () => {
47
+ expect(isValidCronExpression(CronExpression.EVERY_15_MINUTES)).toBe(true);
48
+ });
49
+
50
+ it('EVERY_30_MINUTES should be valid', () => {
51
+ expect(isValidCronExpression(CronExpression.EVERY_30_MINUTES)).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe('hours patterns', () => {
56
+ it('EVERY_HOUR should be valid', () => {
57
+ expect(isValidCronExpression(CronExpression.EVERY_HOUR)).toBe(true);
58
+ });
59
+
60
+ it('EVERY_2_HOURS should be valid', () => {
61
+ expect(isValidCronExpression(CronExpression.EVERY_2_HOURS)).toBe(true);
62
+ });
63
+
64
+ it('EVERY_6_HOURS should be valid', () => {
65
+ expect(isValidCronExpression(CronExpression.EVERY_6_HOURS)).toBe(true);
66
+ });
67
+
68
+ it('EVERY_12_HOURS should be valid', () => {
69
+ expect(isValidCronExpression(CronExpression.EVERY_12_HOURS)).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe('daily patterns', () => {
74
+ it('EVERY_DAY_AT_MIDNIGHT should be valid', () => {
75
+ expect(isValidCronExpression(CronExpression.EVERY_DAY_AT_MIDNIGHT)).toBe(true);
76
+ });
77
+
78
+ it('EVERY_DAY_AT_NOON should be valid', () => {
79
+ expect(isValidCronExpression(CronExpression.EVERY_DAY_AT_NOON)).toBe(true);
80
+ });
81
+
82
+ it('EVERY_DAY_AT_9AM should be valid', () => {
83
+ expect(isValidCronExpression(CronExpression.EVERY_DAY_AT_9AM)).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe('weekly patterns', () => {
88
+ it('EVERY_WEEK should be valid', () => {
89
+ expect(isValidCronExpression(CronExpression.EVERY_WEEK)).toBe(true);
90
+ });
91
+
92
+ it('EVERY_MONDAY should be valid', () => {
93
+ expect(isValidCronExpression(CronExpression.EVERY_MONDAY)).toBe(true);
94
+ });
95
+
96
+ it('EVERY_FRIDAY should be valid', () => {
97
+ expect(isValidCronExpression(CronExpression.EVERY_FRIDAY)).toBe(true);
98
+ });
99
+
100
+ it('EVERY_WEEKDAY should be valid', () => {
101
+ expect(isValidCronExpression(CronExpression.EVERY_WEEKDAY)).toBe(true);
102
+ });
103
+
104
+ it('EVERY_WEEKEND should be valid', () => {
105
+ expect(isValidCronExpression(CronExpression.EVERY_WEEKEND)).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe('monthly/yearly patterns', () => {
110
+ it('EVERY_MONTH should be valid', () => {
111
+ expect(isValidCronExpression(CronExpression.EVERY_MONTH)).toBe(true);
112
+ });
113
+
114
+ it('EVERY_QUARTER should be valid', () => {
115
+ expect(isValidCronExpression(CronExpression.EVERY_QUARTER)).toBe(true);
116
+ });
117
+
118
+ it('EVERY_YEAR should be valid', () => {
119
+ expect(isValidCronExpression(CronExpression.EVERY_YEAR)).toBe(true);
120
+ });
121
+ });
122
+
123
+ describe('pattern relationships', () => {
124
+ it('EVERY_WEEK and EVERY_SUNDAY should be the same (Sunday at midnight)', () => {
125
+ // Both represent Sunday at midnight
126
+ expect(CronExpression.EVERY_WEEK).toBe(CronExpression.EVERY_SUNDAY);
127
+ });
128
+
129
+ it('should have mostly unique values', () => {
130
+ const values = Object.values(CronExpression);
131
+ const uniqueValues = new Set(values);
132
+ // EVERY_WEEK and EVERY_SUNDAY are intentionally the same
133
+ expect(uniqueValues.size).toBe(values.length - 1);
134
+ });
135
+ });
136
+
137
+ describe('all patterns have 6 fields', () => {
138
+ it('should have 6 space-separated fields', () => {
139
+ for (const [_name, pattern] of Object.entries(CronExpression)) {
140
+ const fields = pattern.split(' ');
141
+ expect(fields.length).toBe(6);
142
+ }
143
+ });
144
+ });
145
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Common Cron Expressions
3
+ *
4
+ * Pre-defined cron expressions for common scheduling patterns.
5
+ * All expressions use 6 fields (with seconds).
6
+ */
7
+
8
+ /**
9
+ * Common cron expression patterns
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { CronExpression } from '@onebun/core';
14
+ *
15
+ * @Cron(CronExpression.EVERY_MINUTE, { pattern: 'health.check' })
16
+ * getHealthData() {
17
+ * return { status: 'ok' };
18
+ * }
19
+ * ```
20
+ */
21
+ // eslint-disable-next-line @typescript-eslint/naming-convention
22
+ export const CronExpression = {
23
+ // Seconds
24
+ /** Every second */
25
+ EVERY_SECOND: '* * * * * *',
26
+ /** Every 5 seconds */
27
+ EVERY_5_SECONDS: '*/5 * * * * *',
28
+ /** Every 10 seconds */
29
+ EVERY_10_SECONDS: '*/10 * * * * *',
30
+ /** Every 30 seconds */
31
+ EVERY_30_SECONDS: '*/30 * * * * *',
32
+
33
+ // Minutes
34
+ /** Every minute (at second 0) */
35
+ EVERY_MINUTE: '0 * * * * *',
36
+ /** Every 5 minutes */
37
+ EVERY_5_MINUTES: '0 */5 * * * *',
38
+ /** Every 10 minutes */
39
+ EVERY_10_MINUTES: '0 */10 * * * *',
40
+ /** Every 15 minutes */
41
+ EVERY_15_MINUTES: '0 */15 * * * *',
42
+ /** Every 30 minutes */
43
+ EVERY_30_MINUTES: '0 */30 * * * *',
44
+ /** Every 45 minutes */
45
+ EVERY_45_MINUTES: '0 */45 * * * *',
46
+
47
+ // Hours
48
+ /** Every hour (at minute 0, second 0) */
49
+ EVERY_HOUR: '0 0 * * * *',
50
+ /** Every 2 hours */
51
+ EVERY_2_HOURS: '0 0 */2 * * *',
52
+ /** Every 3 hours */
53
+ EVERY_3_HOURS: '0 0 */3 * * *',
54
+ /** Every 4 hours */
55
+ EVERY_4_HOURS: '0 0 */4 * * *',
56
+ /** Every 6 hours */
57
+ EVERY_6_HOURS: '0 0 */6 * * *',
58
+ /** Every 12 hours */
59
+ EVERY_12_HOURS: '0 0 */12 * * *',
60
+
61
+ // Daily
62
+ /** Every day at midnight (00:00:00) */
63
+ EVERY_DAY_AT_MIDNIGHT: '0 0 0 * * *',
64
+ /** Every day at 1 AM */
65
+ EVERY_DAY_AT_1AM: '0 0 1 * * *',
66
+ /** Every day at 2 AM */
67
+ EVERY_DAY_AT_2AM: '0 0 2 * * *',
68
+ /** Every day at 3 AM */
69
+ EVERY_DAY_AT_3AM: '0 0 3 * * *',
70
+ /** Every day at 6 AM */
71
+ EVERY_DAY_AT_6AM: '0 0 6 * * *',
72
+ /** Every day at 9 AM */
73
+ EVERY_DAY_AT_9AM: '0 0 9 * * *',
74
+ /** Every day at noon (12:00:00) */
75
+ EVERY_DAY_AT_NOON: '0 0 12 * * *',
76
+ /** Every day at 6 PM */
77
+ EVERY_DAY_AT_6PM: '0 0 18 * * *',
78
+
79
+ // Weekly
80
+ /** Every week (Sunday at midnight) */
81
+ EVERY_WEEK: '0 0 0 * * 0',
82
+ /** Every Monday at midnight */
83
+ EVERY_MONDAY: '0 0 0 * * 1',
84
+ /** Every Tuesday at midnight */
85
+ EVERY_TUESDAY: '0 0 0 * * 2',
86
+ /** Every Wednesday at midnight */
87
+ EVERY_WEDNESDAY: '0 0 0 * * 3',
88
+ /** Every Thursday at midnight */
89
+ EVERY_THURSDAY: '0 0 0 * * 4',
90
+ /** Every Friday at midnight */
91
+ EVERY_FRIDAY: '0 0 0 * * 5',
92
+ /** Every Saturday at midnight */
93
+ EVERY_SATURDAY: '0 0 0 * * 6',
94
+ /** Every Sunday at midnight */
95
+ EVERY_SUNDAY: '0 0 0 * * 0',
96
+ /** Every weekday (Monday-Friday) at midnight */
97
+ EVERY_WEEKDAY: '0 0 0 * * 1-5',
98
+ /** Every weekend (Saturday-Sunday) at midnight */
99
+ EVERY_WEEKEND: '0 0 0 * * 0,6',
100
+
101
+ // Monthly
102
+ /** First day of every month at midnight */
103
+ EVERY_MONTH: '0 0 0 1 * *',
104
+ /** First day of every quarter (Jan, Apr, Jul, Oct) at midnight */
105
+ EVERY_QUARTER: '0 0 0 1 1,4,7,10 *',
106
+
107
+ // Yearly
108
+ /** First day of the year at midnight (January 1st) */
109
+ EVERY_YEAR: '0 0 0 1 1 *',
110
+ } as const;
111
+
112
+ /**
113
+ * Type for CronExpression values
114
+ */
115
+ export type CronExpressionValue = (typeof CronExpression)[keyof typeof CronExpression];
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Cron Parser Tests
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ } from 'bun:test';
10
+
11
+ import {
12
+ parseCronExpression,
13
+ getNextRun,
14
+ getNextRuns,
15
+ getMillisecondsUntilNextRun,
16
+ isValidCronExpression,
17
+ } from './cron-parser';
18
+
19
+ describe('cron-parser', () => {
20
+ describe('parseCronExpression', () => {
21
+ it('should parse 5-field expression (without seconds)', () => {
22
+ const schedule = parseCronExpression('* * * * *');
23
+ expect(schedule.seconds).toEqual([0]); // Default seconds = 0
24
+ expect(schedule.minutes.length).toBe(60);
25
+ expect(schedule.hours.length).toBe(24);
26
+ expect(schedule.daysOfMonth.length).toBe(31);
27
+ expect(schedule.months.length).toBe(12);
28
+ expect(schedule.daysOfWeek.length).toBe(7);
29
+ });
30
+
31
+ it('should parse 6-field expression (with seconds)', () => {
32
+ const schedule = parseCronExpression('* * * * * *');
33
+ expect(schedule.seconds.length).toBe(60);
34
+ expect(schedule.minutes.length).toBe(60);
35
+ });
36
+
37
+ it('should parse specific values', () => {
38
+ const schedule = parseCronExpression('0 30 9 15 6 *');
39
+ expect(schedule.seconds).toEqual([0]);
40
+ expect(schedule.minutes).toEqual([30]);
41
+ expect(schedule.hours).toEqual([9]);
42
+ expect(schedule.daysOfMonth).toEqual([15]);
43
+ expect(schedule.months).toEqual([6]);
44
+ });
45
+
46
+ it('should parse step values (*/N)', () => {
47
+ const schedule = parseCronExpression('*/5 */10 * * * *');
48
+ expect(schedule.seconds).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
49
+ expect(schedule.minutes).toEqual([0, 10, 20, 30, 40, 50]);
50
+ });
51
+
52
+ it('should parse ranges (N-M)', () => {
53
+ const schedule = parseCronExpression('0 0 9-17 * * 1-5');
54
+ expect(schedule.hours).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]);
55
+ expect(schedule.daysOfWeek).toEqual([1, 2, 3, 4, 5]);
56
+ });
57
+
58
+ it('should parse lists (N,M,O)', () => {
59
+ const schedule = parseCronExpression('0 0,15,30,45 * * * *');
60
+ expect(schedule.minutes).toEqual([0, 15, 30, 45]);
61
+ });
62
+
63
+ it('should parse combined range and step (N-M/S)', () => {
64
+ const schedule = parseCronExpression('0 0 0-23/2 * * *');
65
+ expect(schedule.hours).toEqual([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]);
66
+ });
67
+
68
+ it('should throw error for invalid expression', () => {
69
+ expect(() => parseCronExpression('invalid')).toThrow();
70
+ expect(() => parseCronExpression('* * *')).toThrow();
71
+ expect(() => parseCronExpression('* * * * * * *')).toThrow();
72
+ });
73
+
74
+ it('should throw error for out of bounds values', () => {
75
+ expect(() => parseCronExpression('60 * * * * *')).toThrow();
76
+ expect(() => parseCronExpression('* 60 * * * *')).toThrow();
77
+ expect(() => parseCronExpression('* * 24 * * *')).toThrow();
78
+ expect(() => parseCronExpression('* * * 32 * *')).toThrow();
79
+ expect(() => parseCronExpression('* * * * 13 *')).toThrow();
80
+ expect(() => parseCronExpression('* * * * * 7')).toThrow();
81
+ });
82
+ });
83
+
84
+ describe('getNextRun', () => {
85
+ it('should find next run time for every minute schedule', () => {
86
+ const schedule = parseCronExpression('0 * * * * *');
87
+ const from = new Date('2024-01-15T10:30:45');
88
+ const next = getNextRun(schedule, from);
89
+
90
+ expect(next).not.toBeNull();
91
+ expect(next!.getMinutes()).toBe(31);
92
+ expect(next!.getSeconds()).toBe(0);
93
+ });
94
+
95
+ it('should find next run time for specific time', () => {
96
+ const schedule = parseCronExpression('0 0 9 * * *');
97
+ const from = new Date('2024-01-15T08:00:00');
98
+ const next = getNextRun(schedule, from);
99
+
100
+ expect(next).not.toBeNull();
101
+ expect(next!.getHours()).toBe(9);
102
+ expect(next!.getMinutes()).toBe(0);
103
+ expect(next!.getDate()).toBe(15);
104
+ });
105
+
106
+ it('should advance to next day if time has passed', () => {
107
+ const schedule = parseCronExpression('0 0 9 * * *');
108
+ const from = new Date('2024-01-15T10:00:00');
109
+ const next = getNextRun(schedule, from);
110
+
111
+ expect(next).not.toBeNull();
112
+ expect(next!.getHours()).toBe(9);
113
+ expect(next!.getDate()).toBe(16);
114
+ });
115
+
116
+ it('should respect day of week constraint', () => {
117
+ const schedule = parseCronExpression('0 0 9 * * 1'); // Monday
118
+ const from = new Date('2024-01-15T08:00:00'); // Monday
119
+ const next = getNextRun(schedule, from);
120
+
121
+ expect(next).not.toBeNull();
122
+ expect(next!.getDay()).toBe(1); // Monday
123
+ });
124
+
125
+ it('should find next run for every 5 seconds', () => {
126
+ const schedule = parseCronExpression('*/5 * * * * *');
127
+ const from = new Date('2024-01-15T10:30:42');
128
+ const next = getNextRun(schedule, from);
129
+
130
+ expect(next).not.toBeNull();
131
+ expect(next!.getSeconds()).toBe(45);
132
+ });
133
+ });
134
+
135
+ describe('getNextRuns', () => {
136
+ it('should return multiple future run times', () => {
137
+ const schedule = parseCronExpression('0 * * * * *');
138
+ const from = new Date('2024-01-15T10:30:00');
139
+ const runs = getNextRuns(schedule, 3, from);
140
+
141
+ expect(runs.length).toBe(3);
142
+ expect(runs[0].getMinutes()).toBe(31);
143
+ expect(runs[1].getMinutes()).toBe(32);
144
+ expect(runs[2].getMinutes()).toBe(33);
145
+ });
146
+
147
+ it('should return empty array for impossible schedule', () => {
148
+ // February 31st doesn't exist
149
+ const schedule = parseCronExpression('0 0 0 31 2 *');
150
+ const runs = getNextRuns(schedule, 3, new Date('2024-01-01'));
151
+
152
+ expect(runs.length).toBe(0);
153
+ });
154
+ });
155
+
156
+ describe('getMillisecondsUntilNextRun', () => {
157
+ it('should calculate milliseconds until next run', () => {
158
+ const from = new Date('2024-01-15T10:30:30.000');
159
+ const ms = getMillisecondsUntilNextRun('0 31 10 * * *', from);
160
+
161
+ expect(ms).not.toBeNull();
162
+ // Should be 30 seconds (from 10:30:30 to 10:31:00)
163
+ expect(ms).toBe(30000);
164
+ });
165
+
166
+ it('should return null for invalid expression', () => {
167
+ expect(() => getMillisecondsUntilNextRun('invalid', new Date())).toThrow();
168
+ });
169
+ });
170
+
171
+ describe('isValidCronExpression', () => {
172
+ it('should return true for valid expressions', () => {
173
+ expect(isValidCronExpression('* * * * *')).toBe(true);
174
+ expect(isValidCronExpression('0 0 9 * * *')).toBe(true);
175
+ expect(isValidCronExpression('*/5 * * * * *')).toBe(true);
176
+ expect(isValidCronExpression('0 0 9-17 * * 1-5')).toBe(true);
177
+ });
178
+
179
+ it('should return false for invalid expressions', () => {
180
+ expect(isValidCronExpression('invalid')).toBe(false);
181
+ expect(isValidCronExpression('* * *')).toBe(false);
182
+ expect(isValidCronExpression('60 * * * * *')).toBe(false);
183
+ });
184
+ });
185
+ });