@engjts/nexus 0.1.8 → 0.1.9

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 (205) hide show
  1. package/package.json +1 -1
  2. package/BENCHMARK_REPORT.md +0 -343
  3. package/documentation/01-getting-started.md +0 -240
  4. package/documentation/02-context.md +0 -335
  5. package/documentation/03-routing.md +0 -397
  6. package/documentation/04-middleware.md +0 -483
  7. package/documentation/05-validation.md +0 -514
  8. package/documentation/06-error-handling.md +0 -465
  9. package/documentation/07-performance.md +0 -364
  10. package/documentation/08-adapters.md +0 -470
  11. package/documentation/09-api-reference.md +0 -548
  12. package/documentation/10-examples.md +0 -582
  13. package/documentation/11-deployment.md +0 -477
  14. package/documentation/12-sentry.md +0 -620
  15. package/documentation/13-sentry-data-storage.md +0 -996
  16. package/documentation/14-sentry-data-reference.md +0 -457
  17. package/documentation/15-sentry-summary.md +0 -409
  18. package/documentation/16-alerts-system.md +0 -745
  19. package/documentation/17-alert-adapters.md +0 -696
  20. package/documentation/18-alerts-implementation-summary.md +0 -385
  21. package/documentation/19-class-based-routing.md +0 -840
  22. package/documentation/20-websocket-realtime.md +0 -813
  23. package/documentation/21-cache-system.md +0 -510
  24. package/documentation/22-job-queue.md +0 -772
  25. package/documentation/23-sentry-plugin.md +0 -551
  26. package/documentation/24-testing-utilities.md +0 -1287
  27. package/documentation/25-api-versioning.md +0 -533
  28. package/documentation/26-context-store.md +0 -607
  29. package/documentation/27-dependency-injection.md +0 -329
  30. package/documentation/28-lifecycle-hooks.md +0 -521
  31. package/documentation/29-package-structure.md +0 -196
  32. package/documentation/30-plugin-system.md +0 -414
  33. package/documentation/31-jwt-authentication.md +0 -597
  34. package/documentation/32-cli.md +0 -268
  35. package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
  36. package/documentation/ALERTS-INDEX.md +0 -330
  37. package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
  38. package/documentation/README.md +0 -178
  39. package/documentation/index.html +0 -34
  40. package/modern_framework_paper.md +0 -1870
  41. package/public/css/style.css +0 -87
  42. package/public/index.html +0 -34
  43. package/public/js/app.js +0 -27
  44. package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
  45. package/src/advanced/cache/MultiTierCache.ts +0 -194
  46. package/src/advanced/cache/RedisCacheStore.ts +0 -341
  47. package/src/advanced/cache/index.ts +0 -5
  48. package/src/advanced/cache/types.ts +0 -40
  49. package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
  50. package/src/advanced/graphql/index.ts +0 -22
  51. package/src/advanced/graphql/server.ts +0 -252
  52. package/src/advanced/graphql/types.ts +0 -42
  53. package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
  54. package/src/advanced/jobs/JobQueue.ts +0 -556
  55. package/src/advanced/jobs/RedisQueueStore.ts +0 -367
  56. package/src/advanced/jobs/index.ts +0 -5
  57. package/src/advanced/jobs/types.ts +0 -70
  58. package/src/advanced/observability/APMManager.ts +0 -163
  59. package/src/advanced/observability/AlertManager.ts +0 -109
  60. package/src/advanced/observability/MetricRegistry.ts +0 -151
  61. package/src/advanced/observability/ObservabilityCenter.ts +0 -304
  62. package/src/advanced/observability/StructuredLogger.ts +0 -154
  63. package/src/advanced/observability/TracingManager.ts +0 -117
  64. package/src/advanced/observability/adapters.ts +0 -304
  65. package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
  66. package/src/advanced/observability/index.ts +0 -11
  67. package/src/advanced/observability/types.ts +0 -174
  68. package/src/advanced/playground/extractPathParams.ts +0 -6
  69. package/src/advanced/playground/generateFieldExample.ts +0 -31
  70. package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
  71. package/src/advanced/playground/generateSummary.ts +0 -19
  72. package/src/advanced/playground/getTagFromPath.ts +0 -9
  73. package/src/advanced/playground/index.ts +0 -8
  74. package/src/advanced/playground/playground.ts +0 -250
  75. package/src/advanced/playground/types.ts +0 -49
  76. package/src/advanced/playground/zodToExample.ts +0 -16
  77. package/src/advanced/playground/zodToParams.ts +0 -15
  78. package/src/advanced/postman/buildAuth.ts +0 -31
  79. package/src/advanced/postman/buildBody.ts +0 -15
  80. package/src/advanced/postman/buildQueryParams.ts +0 -27
  81. package/src/advanced/postman/buildRequestItem.ts +0 -36
  82. package/src/advanced/postman/buildResponses.ts +0 -11
  83. package/src/advanced/postman/buildUrl.ts +0 -33
  84. package/src/advanced/postman/capitalize.ts +0 -4
  85. package/src/advanced/postman/generateCollection.ts +0 -59
  86. package/src/advanced/postman/generateEnvironment.ts +0 -34
  87. package/src/advanced/postman/generateExampleFromZod.ts +0 -21
  88. package/src/advanced/postman/generateFieldExample.ts +0 -45
  89. package/src/advanced/postman/generateName.ts +0 -20
  90. package/src/advanced/postman/generateUUID.ts +0 -11
  91. package/src/advanced/postman/getTagFromPath.ts +0 -10
  92. package/src/advanced/postman/index.ts +0 -28
  93. package/src/advanced/postman/postman.ts +0 -156
  94. package/src/advanced/postman/slugify.ts +0 -7
  95. package/src/advanced/postman/types.ts +0 -140
  96. package/src/advanced/realtime/index.ts +0 -18
  97. package/src/advanced/realtime/websocket.ts +0 -231
  98. package/src/advanced/sentry/index.ts +0 -1236
  99. package/src/advanced/sentry/types.ts +0 -355
  100. package/src/advanced/static/generateDirectoryListing.ts +0 -47
  101. package/src/advanced/static/generateETag.ts +0 -7
  102. package/src/advanced/static/getMimeType.ts +0 -9
  103. package/src/advanced/static/index.ts +0 -32
  104. package/src/advanced/static/isSafePath.ts +0 -13
  105. package/src/advanced/static/publicDir.ts +0 -21
  106. package/src/advanced/static/serveStatic.ts +0 -225
  107. package/src/advanced/static/spa.ts +0 -24
  108. package/src/advanced/static/types.ts +0 -159
  109. package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
  110. package/src/advanced/swagger/buildOperation.ts +0 -61
  111. package/src/advanced/swagger/buildParameters.ts +0 -61
  112. package/src/advanced/swagger/buildRequestBody.ts +0 -21
  113. package/src/advanced/swagger/buildResponses.ts +0 -54
  114. package/src/advanced/swagger/capitalize.ts +0 -5
  115. package/src/advanced/swagger/convertPath.ts +0 -9
  116. package/src/advanced/swagger/createSwagger.ts +0 -12
  117. package/src/advanced/swagger/generateOperationId.ts +0 -21
  118. package/src/advanced/swagger/generateSpec.ts +0 -105
  119. package/src/advanced/swagger/generateSummary.ts +0 -24
  120. package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
  121. package/src/advanced/swagger/generateThemeCss.ts +0 -53
  122. package/src/advanced/swagger/index.ts +0 -25
  123. package/src/advanced/swagger/swagger.ts +0 -237
  124. package/src/advanced/swagger/types.ts +0 -206
  125. package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
  126. package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
  127. package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
  128. package/src/advanced/testing/factory.ts +0 -509
  129. package/src/advanced/testing/harness.ts +0 -612
  130. package/src/advanced/testing/index.ts +0 -430
  131. package/src/advanced/testing/load-test.ts +0 -618
  132. package/src/advanced/testing/mock-server.ts +0 -498
  133. package/src/advanced/testing/mock.ts +0 -670
  134. package/src/cli/bin.ts +0 -9
  135. package/src/cli/cli.ts +0 -158
  136. package/src/cli/commands/add.ts +0 -178
  137. package/src/cli/commands/build.ts +0 -73
  138. package/src/cli/commands/create.ts +0 -166
  139. package/src/cli/commands/dev.ts +0 -85
  140. package/src/cli/commands/generate.ts +0 -99
  141. package/src/cli/commands/help.ts +0 -95
  142. package/src/cli/commands/init.ts +0 -91
  143. package/src/cli/commands/version.ts +0 -38
  144. package/src/cli/index.ts +0 -6
  145. package/src/cli/templates/generators.ts +0 -359
  146. package/src/cli/templates/index.ts +0 -680
  147. package/src/cli/utils/exec.ts +0 -52
  148. package/src/cli/utils/file-system.ts +0 -78
  149. package/src/cli/utils/logger.ts +0 -111
  150. package/src/core/adapter.ts +0 -88
  151. package/src/core/application.ts +0 -1453
  152. package/src/core/context-pool.ts +0 -79
  153. package/src/core/context.ts +0 -856
  154. package/src/core/index.ts +0 -94
  155. package/src/core/middleware.ts +0 -272
  156. package/src/core/performance/buffer-pool.ts +0 -108
  157. package/src/core/performance/middleware-optimizer.ts +0 -162
  158. package/src/core/plugin/PluginManager.ts +0 -435
  159. package/src/core/plugin/builder.ts +0 -358
  160. package/src/core/plugin/index.ts +0 -50
  161. package/src/core/plugin/types.ts +0 -214
  162. package/src/core/router/file-router.ts +0 -623
  163. package/src/core/router/index.ts +0 -260
  164. package/src/core/router/radix-tree.ts +0 -242
  165. package/src/core/serializer.ts +0 -397
  166. package/src/core/store/index.ts +0 -30
  167. package/src/core/store/registry.ts +0 -178
  168. package/src/core/store/request-store.ts +0 -240
  169. package/src/core/store/types.ts +0 -233
  170. package/src/core/types.ts +0 -616
  171. package/src/database/adapter.ts +0 -35
  172. package/src/database/adapters/index.ts +0 -1
  173. package/src/database/adapters/mysql.ts +0 -669
  174. package/src/database/database.ts +0 -70
  175. package/src/database/dialect.ts +0 -388
  176. package/src/database/index.ts +0 -12
  177. package/src/database/migrations.ts +0 -86
  178. package/src/database/optimizer.ts +0 -125
  179. package/src/database/query-builder.ts +0 -404
  180. package/src/database/realtime.ts +0 -53
  181. package/src/database/schema.ts +0 -71
  182. package/src/database/transactions.ts +0 -56
  183. package/src/database/types.ts +0 -87
  184. package/src/deployment/cluster.ts +0 -471
  185. package/src/deployment/config.ts +0 -454
  186. package/src/deployment/docker.ts +0 -599
  187. package/src/deployment/graceful-shutdown.ts +0 -373
  188. package/src/deployment/index.ts +0 -56
  189. package/src/index.ts +0 -281
  190. package/src/security/adapter.ts +0 -318
  191. package/src/security/auth/JWTPlugin.ts +0 -234
  192. package/src/security/auth/JWTProvider.ts +0 -316
  193. package/src/security/auth/adapter.ts +0 -12
  194. package/src/security/auth/jwt.ts +0 -234
  195. package/src/security/auth/middleware.ts +0 -188
  196. package/src/security/csrf.ts +0 -220
  197. package/src/security/headers.ts +0 -108
  198. package/src/security/index.ts +0 -60
  199. package/src/security/rate-limit/adapter.ts +0 -7
  200. package/src/security/rate-limit/memory.ts +0 -108
  201. package/src/security/rate-limit/middleware.ts +0 -181
  202. package/src/security/sanitization.ts +0 -75
  203. package/src/security/types.ts +0 -240
  204. package/src/security/utils.ts +0 -52
  205. package/tsconfig.json +0 -39
@@ -1,42 +0,0 @@
1
- import { GraphQLSchema } from "graphql";
2
- import { Context } from "vm";
3
- import { MultiTierCache } from "../cache";
4
- import { SimpleDataLoader } from "./SimpleDataLoader";
5
-
6
- export interface GraphQLComplexityOptions {
7
- limit: number;
8
- cost?: Record<string, number>;
9
- defaultCost?: number;
10
- }
11
-
12
- export interface GraphQLCacheOptions {
13
- instance: MultiTierCache<any>;
14
- ttl?: number;
15
- keyGenerator?: (payload: GraphQLRequestPayload) => string;
16
- }
17
-
18
- export interface GraphQLRequestPayload {
19
- query?: string;
20
- variables?: Record<string, any>;
21
- operationName?: string;
22
- }
23
-
24
- export interface GraphQLServerOptions {
25
- schema: GraphQLSchema;
26
- context?: (args: { ctx: Context }) => Promise<Record<string, any>> | Record<string, any>;
27
- playground?: boolean;
28
- introspection?: boolean;
29
- cache?: GraphQLCacheOptions;
30
- complexity?: GraphQLComplexityOptions;
31
- depthLimit?: number;
32
- dataloaders?: boolean;
33
- formatError?: (error: any) => any;
34
- logger?: { debug?: (...args: any[]) => void; error?: (...args: any[]) => void };
35
- }
36
-
37
- export interface SimpleDataLoaderBatch<Key = any, Value = any> {
38
- keys: Key[];
39
- resolvers: Array<{ resolve: (value: Value) => void; reject: (error: Error) => void }>;
40
- }
41
-
42
- export type DataLoaderFactory = (ctx: Context) => Record<string, SimpleDataLoader>;
@@ -1,68 +0,0 @@
1
- import { QueueStore, Job, JobState, QueueStats } from './types';
2
-
3
- /**
4
- * Simple in-memory queue store implementation
5
- */
6
-
7
-
8
- export class InMemoryQueueStore<Data = any> implements QueueStore<Data> {
9
- private jobs: Map<string, Job<Data>> = new Map();
10
-
11
- async enqueue(job: Job<Data>): Promise<void> {
12
- this.jobs.set(job.id, job);
13
- }
14
-
15
- async dequeue(): Promise<Job<Data> | undefined> {
16
- const candidates = Array.from(this.jobs.values())
17
- .filter(job => job.state === 'waiting' || (job.state === 'delayed' && job.runAt <= Date.now()))
18
- .sort((a, b) => {
19
- if (a.priority === b.priority) {
20
- return a.runAt - b.runAt;
21
- }
22
- return b.priority - a.priority;
23
- });
24
-
25
- const job = candidates[0];
26
- if (!job) {
27
- return undefined;
28
- }
29
-
30
- job.state = 'active';
31
- job.updatedAt = Date.now();
32
- this.jobs.set(job.id, job);
33
- return job;
34
- }
35
-
36
- async update(job: Job<Data>): Promise<void> {
37
- this.jobs.set(job.id, job);
38
- }
39
-
40
- async get(id: string): Promise<Job<Data> | undefined> {
41
- return this.jobs.get(id);
42
- }
43
-
44
- async list(state?: JobState): Promise<Job<Data>[]> {
45
- if (!state) {
46
- return Array.from(this.jobs.values());
47
- }
48
- return Array.from(this.jobs.values()).filter(job => job.state === state);
49
- }
50
-
51
- async stats(): Promise<QueueStats> {
52
- const stats: QueueStats = {
53
- waiting: 0,
54
- active: 0,
55
- completed: 0,
56
- failed: 0,
57
- delayed: 0
58
- };
59
-
60
- for (const job of this.jobs.values()) {
61
- if (job.state in stats) {
62
- (stats as any)[job.state] += 1;
63
- }
64
- }
65
-
66
- return stats;
67
- }
68
- }
@@ -1,556 +0,0 @@
1
- import { randomUUID } from 'crypto';
2
- import { EventEmitter } from 'events';
3
- import { InMemoryQueueStore } from './InMemoryQueueStore';
4
- import { QueueOptions, JobHandler, QueueStore, EnqueueOptions, Job, QueueStats } from './types';
5
-
6
- /**
7
- * Cron expression parser
8
- * Supports: second minute hour dayOfMonth month dayOfWeek
9
- *
10
- * Examples:
11
- * - '0 0 * * *' = Every day at midnight
12
- * - '*\/5 * * * *' = Every 5 minutes
13
- * - '0 9 * * 1-5' = 9 AM on weekdays
14
- * - '0 0 1 * *' = First day of every month
15
- */
16
- export interface CronSchedule {
17
- /**
18
- * Cron expression (5 or 6 fields)
19
- * Standard: minute hour dayOfMonth month dayOfWeek
20
- * Extended: second minute hour dayOfMonth month dayOfWeek
21
- */
22
- cron: string;
23
-
24
- /**
25
- * Timezone for the schedule (e.g., 'America/New_York')
26
- * @default 'UTC'
27
- */
28
- timezone?: string;
29
- }
30
-
31
- /**
32
- * Scheduled job configuration
33
- */
34
- export interface ScheduledJobConfig<Data = any> {
35
- /**
36
- * Unique name for the scheduled job
37
- */
38
- name: string;
39
-
40
- /**
41
- * Cron schedule or interval
42
- */
43
- schedule: CronSchedule | number;
44
-
45
- /**
46
- * Job data generator or static data
47
- */
48
- data: Data | (() => Data | Promise<Data>);
49
-
50
- /**
51
- * Job handler
52
- */
53
- handler: JobHandler<Data, any>;
54
-
55
- /**
56
- * Whether to run immediately on startup
57
- * @default false
58
- */
59
- runOnStart?: boolean;
60
-
61
- /**
62
- * Maximum instances that can run concurrently
63
- * @default 1
64
- */
65
- maxConcurrency?: number;
66
- }
67
-
68
- /**
69
- * Simple cron parser
70
- */
71
- function parseCron(expression: string): { next: (from?: Date) => Date } {
72
- const parts = expression.trim().split(/\s+/);
73
-
74
- // Support both 5-field and 6-field cron expressions
75
- let minute: string, hour: string, dayOfMonth: string, month: string, dayOfWeek: string;
76
-
77
- if (parts.length === 5) {
78
- [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
79
- } else if (parts.length === 6) {
80
- // 6-field includes seconds, we ignore it for simplicity
81
- [, minute, hour, dayOfMonth, month, dayOfWeek] = parts;
82
- } else {
83
- throw new Error(`Invalid cron expression: ${expression}`);
84
- }
85
-
86
- function parseField(field: string, min: number, max: number): number[] {
87
- const values: Set<number> = new Set();
88
-
89
- for (const part of field.split(',')) {
90
- if (part === '*') {
91
- for (let i = min; i <= max; i++) values.add(i);
92
- } else if (part.includes('/')) {
93
- const [range, step] = part.split('/');
94
- const stepNum = parseInt(step, 10);
95
- const [start, end] = range === '*'
96
- ? [min, max]
97
- : range.includes('-')
98
- ? range.split('-').map(n => parseInt(n, 10))
99
- : [parseInt(range, 10), max];
100
- for (let i = start; i <= end; i += stepNum) values.add(i);
101
- } else if (part.includes('-')) {
102
- const [start, end] = part.split('-').map(n => parseInt(n, 10));
103
- for (let i = start; i <= end; i++) values.add(i);
104
- } else {
105
- values.add(parseInt(part, 10));
106
- }
107
- }
108
-
109
- return Array.from(values).sort((a, b) => a - b);
110
- }
111
-
112
- const minutes = parseField(minute, 0, 59);
113
- const hours = parseField(hour, 0, 23);
114
- const daysOfMonth = parseField(dayOfMonth, 1, 31);
115
- const months = parseField(month, 1, 12);
116
- const daysOfWeek = parseField(dayOfWeek, 0, 6);
117
-
118
- return {
119
- next(from: Date = new Date()): Date {
120
- const date = new Date(from);
121
- date.setSeconds(0, 0);
122
- date.setMinutes(date.getMinutes() + 1);
123
-
124
- // Find next matching time (max 1 year search)
125
- const maxIterations = 366 * 24 * 60;
126
- for (let i = 0; i < maxIterations; i++) {
127
- const m = date.getMonth() + 1;
128
- const dom = date.getDate();
129
- const dow = date.getDay();
130
- const h = date.getHours();
131
- const min = date.getMinutes();
132
-
133
- if (
134
- months.includes(m) &&
135
- daysOfMonth.includes(dom) &&
136
- daysOfWeek.includes(dow) &&
137
- hours.includes(h) &&
138
- minutes.includes(min)
139
- ) {
140
- return date;
141
- }
142
-
143
- date.setMinutes(date.getMinutes() + 1);
144
- }
145
-
146
- throw new Error('Could not find next cron execution time within 1 year');
147
- }
148
- };
149
- }
150
-
151
- /**
152
- * Background job queue implementation with cron support
153
- *
154
- * @example
155
- * ```typescript
156
- * const queue = new JobQueue('tasks');
157
- *
158
- * // Regular job
159
- * queue.process('sendEmail', async (job) => {
160
- * await sendEmail(job.data);
161
- * });
162
- *
163
- * // Scheduled job (every 5 minutes)
164
- * queue.schedule({
165
- * name: 'cleanup',
166
- * schedule: { cron: '*\/5 * * * *' },
167
- * data: {},
168
- * handler: async () => {
169
- * await cleanupOldData();
170
- * }
171
- * });
172
- *
173
- * // Interval job (every 30 seconds)
174
- * queue.schedule({
175
- * name: 'healthCheck',
176
- * schedule: 30000,
177
- * data: {},
178
- * handler: async () => {
179
- * await checkHealth();
180
- * }
181
- * });
182
- * ```
183
- */
184
- export class JobQueue<Data = any, Result = any> extends EventEmitter {
185
- name: string;
186
- private options: Required<Pick<QueueOptions<Data>, 'concurrency' | 'retry'>> & QueueOptions<Data>;
187
- private handlers: Map<string, JobHandler<Data, Result>> = new Map();
188
- private store: QueueStore<Data>;
189
- private activeWorkers = 0;
190
- private paused = false;
191
- private limiterWindowStart = 0;
192
- private processedInWindow = 0;
193
-
194
- // Scheduler state
195
- private scheduledJobs: Map<string, {
196
- config: ScheduledJobConfig<Data>;
197
- timer?: NodeJS.Timeout;
198
- activeCount: number;
199
- }> = new Map();
200
-
201
- constructor(name: string, options: QueueOptions<Data> = {}) {
202
- super();
203
- this.name = name;
204
- this.options = {
205
- concurrency: options.concurrency ?? 5,
206
- retry: {
207
- attempts: options.retry?.attempts ?? 3,
208
- backoff: options.retry?.backoff ?? 'exponential',
209
- delay: options.retry?.delay ?? 1000,
210
- maxDelay: options.retry?.maxDelay ?? 60000
211
- },
212
- ...options
213
- };
214
-
215
- this.store = options.store ?? new InMemoryQueueStore<Data>();
216
- }
217
-
218
- /**
219
- * Add a job to the queue
220
- */
221
- async add(
222
- name: string,
223
- data: Data,
224
- options: EnqueueOptions = {}
225
- ): Promise<Job<Data>> {
226
- if (!this.handlers.has(name)) {
227
- throw new Error(`No handler registered for job "${name}"`);
228
- }
229
-
230
- const now = Date.now();
231
- const job: Job<Data> = {
232
- id: randomUUID(),
233
- name,
234
- data,
235
- state: options.delay ? 'delayed' : 'waiting',
236
- attemptsMade: 0,
237
- maxAttempts: options.attempts ?? this.options.retry.attempts ?? 3,
238
- priority: options.priority ?? 0,
239
- createdAt: now,
240
- updatedAt: now,
241
- runAt: options.delay ? now + options.delay : now,
242
- metadata: options.metadata
243
- };
244
-
245
- await this.store.enqueue(job);
246
- this.emit('added', job);
247
- void this.work();
248
- return job;
249
- }
250
-
251
- /**
252
- * Add many jobs at once
253
- */
254
- async addBulk(jobs: Array<{ name: string; data: Data; options?: EnqueueOptions; }>) {
255
- return Promise.all(jobs.map(job => this.add(job.name, job.data, job.options)));
256
- }
257
-
258
- /**
259
- * Register a processor for a given job name
260
- */
261
- process(name: string, handler: JobHandler<Data, Result>) {
262
- this.handlers.set(name, handler);
263
- }
264
-
265
- /**
266
- * Schedule a recurring job using cron expression or interval
267
- *
268
- * @example
269
- * ```typescript
270
- * // Cron: Every day at 9 AM
271
- * queue.schedule({
272
- * name: 'dailyReport',
273
- * schedule: { cron: '0 9 * * *' },
274
- * data: { type: 'daily' },
275
- * handler: async (job) => generateReport(job.data)
276
- * });
277
- *
278
- * // Interval: Every 30 seconds
279
- * queue.schedule({
280
- * name: 'heartbeat',
281
- * schedule: 30000,
282
- * data: {},
283
- * handler: async () => sendHeartbeat()
284
- * });
285
- * ```
286
- */
287
- schedule(config: ScheduledJobConfig<Data>): void {
288
- const { name, schedule, handler, runOnStart = false, maxConcurrency = 1 } = config;
289
-
290
- if (this.scheduledJobs.has(name)) {
291
- throw new Error(`Scheduled job "${name}" already exists. Call unschedule() first.`);
292
- }
293
-
294
- // Register the handler
295
- this.handlers.set(name, handler);
296
-
297
- const state = {
298
- config,
299
- timer: undefined as NodeJS.Timeout | undefined,
300
- activeCount: 0
301
- };
302
-
303
- this.scheduledJobs.set(name, state);
304
-
305
- const executeScheduledJob = async () => {
306
- if (this.paused) return;
307
- if (state.activeCount >= maxConcurrency) {
308
- this.emit('schedule:skipped', name, 'max concurrency reached');
309
- return;
310
- }
311
-
312
- state.activeCount++;
313
- try {
314
- const data = typeof config.data === 'function'
315
- ? await (config.data as () => Data | Promise<Data>)()
316
- : config.data;
317
-
318
- await this.add(name, data);
319
- this.emit('schedule:triggered', name);
320
- } catch (error) {
321
- this.emit('schedule:error', name, error);
322
- } finally {
323
- state.activeCount--;
324
- }
325
- };
326
-
327
- if (typeof schedule === 'number') {
328
- // Interval-based scheduling
329
- if (runOnStart) {
330
- void executeScheduledJob();
331
- }
332
- state.timer = setInterval(executeScheduledJob, schedule);
333
- } else {
334
- // Cron-based scheduling
335
- const cron = parseCron(schedule.cron);
336
-
337
- const scheduleNext = () => {
338
- const now = new Date();
339
- const next = cron.next(now);
340
- const delay = next.getTime() - now.getTime();
341
-
342
- state.timer = setTimeout(async () => {
343
- await executeScheduledJob();
344
- scheduleNext();
345
- }, delay);
346
-
347
- this.emit('schedule:next', name, next);
348
- };
349
-
350
- if (runOnStart) {
351
- void executeScheduledJob();
352
- }
353
- scheduleNext();
354
- }
355
-
356
- this.emit('schedule:registered', name, schedule);
357
- }
358
-
359
- /**
360
- * Remove a scheduled job
361
- */
362
- unschedule(name: string): boolean {
363
- const state = this.scheduledJobs.get(name);
364
- if (!state) {
365
- return false;
366
- }
367
-
368
- if (state.timer) {
369
- clearTimeout(state.timer);
370
- clearInterval(state.timer);
371
- }
372
-
373
- this.scheduledJobs.delete(name);
374
- this.emit('schedule:removed', name);
375
- return true;
376
- }
377
-
378
- /**
379
- * Get all scheduled jobs
380
- */
381
- getScheduledJobs(): string[] {
382
- return Array.from(this.scheduledJobs.keys());
383
- }
384
-
385
- /**
386
- * Pause job processing
387
- */
388
- pause() {
389
- this.paused = true;
390
- this.emit('paused');
391
- }
392
-
393
- /**
394
- * Resume job processing
395
- */
396
- resume() {
397
- if (!this.paused) return;
398
- this.paused = false;
399
- this.emit('resumed');
400
- void this.work();
401
- }
402
-
403
- /**
404
- * Get queue statistics
405
- */
406
- async stats(): Promise<QueueStats> {
407
- return this.store.stats();
408
- }
409
-
410
- /**
411
- * Retry a failed job manually
412
- */
413
- async retry(jobId: string): Promise<void> {
414
- const job = await this.store.get(jobId);
415
- if (!job) {
416
- throw new Error(`Job ${jobId} not found`);
417
- }
418
- job.state = 'waiting';
419
- job.runAt = Date.now();
420
- job.updatedAt = Date.now();
421
- await this.store.update(job);
422
- void this.work();
423
- }
424
-
425
- /**
426
- * Shutdown the queue gracefully
427
- */
428
- async shutdown(): Promise<void> {
429
- this.paused = true;
430
-
431
- // Clear all scheduled jobs
432
- for (const [name] of this.scheduledJobs) {
433
- this.unschedule(name);
434
- }
435
-
436
- // Wait for active workers to finish
437
- const maxWait = 30000;
438
- const startTime = Date.now();
439
- while (this.activeWorkers > 0 && Date.now() - startTime < maxWait) {
440
- await new Promise(resolve => setTimeout(resolve, 100));
441
- }
442
-
443
- this.emit('shutdown');
444
- }
445
-
446
- /**
447
- * Internal worker loop
448
- */
449
- private async work(): Promise<void> {
450
- if (this.paused) {
451
- return;
452
- }
453
-
454
- while (this.activeWorkers < (this.options.concurrency ?? 1)) {
455
- if (!this.canProcessDueToRateLimit()) {
456
- break;
457
- }
458
-
459
- const job = await this.store.dequeue();
460
- if (!job) {
461
- break;
462
- }
463
-
464
- this.activeWorkers++;
465
- void this.executeJob(job);
466
- }
467
- }
468
-
469
- private async executeJob(job: Job<Data>) {
470
- const handler = this.handlers.get(job.name);
471
- if (!handler) {
472
- await this.failJob(job, new Error(`Handler missing for job "${job.name}"`));
473
- return;
474
- }
475
-
476
- this.emit('active', job);
477
-
478
- try {
479
- const result = await handler(job);
480
- await this.completeJob(job, result);
481
- } catch (error) {
482
- await this.failJob(job, error as Error);
483
- } finally {
484
- this.activeWorkers = Math.max(0, this.activeWorkers - 1);
485
- this.processedInWindow++;
486
- if (!this.paused) {
487
- void this.work();
488
- }
489
- }
490
- }
491
-
492
- private async completeJob(job: Job<Data>, result: Result) {
493
- job.state = 'completed';
494
- job.result = result;
495
- job.updatedAt = Date.now();
496
- await this.store.update(job);
497
-
498
- this.emit('completed', job, result);
499
- await this.options.hooks?.onComplete?.(job, result);
500
- }
501
-
502
- private async failJob(job: Job<Data>, error: Error) {
503
- job.attemptsMade++;
504
- job.error = { message: error.message, stack: error.stack };
505
- job.updatedAt = Date.now();
506
-
507
- if (job.attemptsMade < job.maxAttempts) {
508
- const delay = this.calculateRetryDelay(job.attemptsMade);
509
- job.state = 'delayed';
510
- job.runAt = Date.now() + delay;
511
- await this.store.update(job);
512
- this.emit('retrying', job, job.attemptsMade, delay);
513
- await this.options.hooks?.onRetry?.(job, job.attemptsMade, delay);
514
- } else {
515
- job.state = 'failed';
516
- await this.store.update(job);
517
- this.emit('failed', job, error);
518
- await this.options.hooks?.onFailed?.(job, error);
519
- }
520
- }
521
-
522
- private calculateRetryDelay(attempt: number): number {
523
- const { backoff = 'exponential', delay = 1000, maxDelay = 60000 } = this.options.retry ?? {};
524
- if (backoff === 'fixed') {
525
- return Math.min(delay, maxDelay);
526
- }
527
- const computed = delay * Math.pow(2, attempt - 1);
528
- return Math.min(computed, maxDelay);
529
- }
530
-
531
- private canProcessDueToRateLimit(): boolean {
532
- const limiter = this.options.limiter;
533
- if (!limiter) {
534
- return true;
535
- }
536
-
537
- const now = Date.now();
538
-
539
- if (now - this.limiterWindowStart > limiter.duration) {
540
- this.limiterWindowStart = now;
541
- this.processedInWindow = 0;
542
- }
543
-
544
- if (this.processedInWindow >= limiter.max) {
545
- const waitTime = limiter.duration - (now - this.limiterWindowStart);
546
- setTimeout(() => {
547
- this.processedInWindow = 0;
548
- this.limiterWindowStart = Date.now();
549
- void this.work();
550
- }, waitTime);
551
- return false;
552
- }
553
-
554
- return true;
555
- }
556
- }