@veloxts/core 0.7.5 → 0.7.7
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/CHANGELOG.md +12 -0
- package/dist/app.js +8 -8
- package/dist/errors/catalog.js +37 -0
- package/dist/errors.d.ts +165 -2
- package/dist/errors.js +207 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +8 -2
- package/dist/plugin.d.ts +52 -0
- package/dist/plugin.js +63 -0
- package/dist/plugins/raw-body.d.ts +37 -0
- package/dist/plugins/raw-body.js +48 -0
- package/dist/utils/fire-and-forget.d.ts +33 -0
- package/dist/utils/fire-and-forget.js +40 -0
- package/dist/utils/retry.d.ts +41 -0
- package/dist/utils/retry.js +48 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @veloxts/core
|
|
2
2
|
|
|
3
|
+
## 0.7.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- refactor(router): rename swaggerUIPlugin → swaggerPlugin, remove redundant exports
|
|
8
|
+
|
|
9
|
+
## 0.7.6
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- feat(router): custom access levels for the Resource API + advanced Architectural Patterns
|
|
14
|
+
|
|
3
15
|
## 0.7.5
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/app.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import fastify from 'fastify';
|
|
7
7
|
import fp from 'fastify-plugin';
|
|
8
8
|
import { setupContextHook } from './context.js';
|
|
9
|
-
import { isVeloxError, VeloxError } from './errors.js';
|
|
9
|
+
import { ConflictError, isVeloxError, VeloxError } from './errors.js';
|
|
10
10
|
import { isFastifyPlugin, isVeloxPlugin, validatePluginMetadata } from './plugin.js';
|
|
11
11
|
import { requestLogger } from './plugins/request-logger.js';
|
|
12
12
|
import { registerStatic } from './plugins/static.js';
|
|
@@ -136,13 +136,13 @@ export class VeloxApp {
|
|
|
136
136
|
if (error.name === 'PrismaClientKnownRequestError' &&
|
|
137
137
|
'code' in error &&
|
|
138
138
|
error.code === 'P2002') {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
const meta = 'meta' in error && typeof error.meta === 'object' && error.meta !== null
|
|
140
|
+
? error.meta
|
|
141
|
+
: undefined;
|
|
142
|
+
const target = Array.isArray(meta?.target) ? meta.target : [];
|
|
143
|
+
const fieldNames = target.length > 0 ? target.join(', ') : 'field';
|
|
144
|
+
const conflictError = new ConflictError(`A record with this ${fieldNames} already exists`, target.length > 0 ? target : undefined);
|
|
145
|
+
return reply.status(conflictError.statusCode).send(conflictError.toJSON());
|
|
146
146
|
}
|
|
147
147
|
let statusCode = 500;
|
|
148
148
|
if (isVeloxError(error)) {
|
package/dist/errors/catalog.js
CHANGED
|
@@ -415,6 +415,43 @@ await app.register(databasePlugin({ client: prisma }));
|
|
|
415
415
|
},
|
|
416
416
|
docsUrl: 'https://veloxts.dev/errors/VELOX-4003',
|
|
417
417
|
},
|
|
418
|
+
'VELOX-4004': {
|
|
419
|
+
code: 'VELOX-4004',
|
|
420
|
+
title: 'Unique Constraint Violation',
|
|
421
|
+
description: 'A database operation failed because it would create a duplicate record.',
|
|
422
|
+
statusCode: 409,
|
|
423
|
+
fix: {
|
|
424
|
+
suggestion: 'Check for existing records before creating, or use upsert.',
|
|
425
|
+
example: `// Use upsert to handle duplicates gracefully
|
|
426
|
+
const user = await prisma.user.upsert({
|
|
427
|
+
where: { email: input.email },
|
|
428
|
+
create: input,
|
|
429
|
+
update: input,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Or use the db() wrapper from @veloxts/orm
|
|
433
|
+
import { db } from '@veloxts/orm';
|
|
434
|
+
const user = await db(() => prisma.user.create({ data: input }));
|
|
435
|
+
// Automatically throws ConflictError with field names`,
|
|
436
|
+
},
|
|
437
|
+
docsUrl: 'https://veloxts.dev/errors/VELOX-4004',
|
|
438
|
+
},
|
|
439
|
+
'VELOX-4005': {
|
|
440
|
+
code: 'VELOX-4005',
|
|
441
|
+
title: 'Foreign Key Constraint Violation',
|
|
442
|
+
description: 'A database operation failed because a related record does not exist.',
|
|
443
|
+
statusCode: 400,
|
|
444
|
+
fix: {
|
|
445
|
+
suggestion: 'Verify that all referenced records exist before creating or updating.',
|
|
446
|
+
example: `// Check related record exists first
|
|
447
|
+
const post = await prisma.post.findUnique({ where: { id: input.postId } });
|
|
448
|
+
if (!post) throw new NotFoundError('Post', input.postId);
|
|
449
|
+
|
|
450
|
+
// Then create the comment
|
|
451
|
+
const comment = await prisma.comment.create({ data: input });`,
|
|
452
|
+
},
|
|
453
|
+
docsUrl: 'https://veloxts.dev/errors/VELOX-4005',
|
|
454
|
+
},
|
|
418
455
|
// ==========================================================================
|
|
419
456
|
// VALIDATION ERRORS (5XXX)
|
|
420
457
|
// ==========================================================================
|
package/dist/errors.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { ERROR_CATALOG, ERROR_DOMAINS, type ErrorCatalogEntry, type ErrorCode, t
|
|
|
8
8
|
* Known error codes in the VeloxTS framework core
|
|
9
9
|
* Can be extended via declaration merging by plugins
|
|
10
10
|
*/
|
|
11
|
-
export type VeloxCoreErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'CONFIGURATION_ERROR' | 'PLUGIN_REGISTRATION_ERROR' | 'SERVER_ALREADY_RUNNING' | 'SERVER_NOT_RUNNING' | 'SERVER_START_ERROR' | 'SERVER_STOP_ERROR' | 'INVALID_PLUGIN_METADATA';
|
|
11
|
+
export type VeloxCoreErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'CONFLICT' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'SERVICE_UNAVAILABLE' | 'RATE_LIMITED' | 'UNPROCESSABLE' | 'CONFIGURATION_ERROR' | 'PLUGIN_REGISTRATION_ERROR' | 'SERVER_ALREADY_RUNNING' | 'SERVER_NOT_RUNNING' | 'SERVER_START_ERROR' | 'SERVER_STOP_ERROR' | 'INVALID_PLUGIN_METADATA';
|
|
12
12
|
/**
|
|
13
13
|
* Registry for error codes - allows plugins to extend via declaration merging
|
|
14
14
|
*
|
|
@@ -64,6 +64,56 @@ export interface NotFoundErrorResponse extends BaseErrorResponse {
|
|
|
64
64
|
/** Optional identifier of the resource */
|
|
65
65
|
resourceId?: string;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Conflict error response with field details
|
|
69
|
+
*/
|
|
70
|
+
export interface ConflictErrorResponse extends BaseErrorResponse {
|
|
71
|
+
error: 'ConflictError';
|
|
72
|
+
statusCode: 409;
|
|
73
|
+
code: 'CONFLICT';
|
|
74
|
+
fields?: string[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Too many requests error response with retry timing
|
|
78
|
+
*/
|
|
79
|
+
export interface TooManyRequestsErrorResponse extends BaseErrorResponse {
|
|
80
|
+
error: 'TooManyRequestsError';
|
|
81
|
+
statusCode: 429;
|
|
82
|
+
code: 'RATE_LIMITED';
|
|
83
|
+
retryAfter?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Forbidden error response
|
|
87
|
+
*/
|
|
88
|
+
export interface ForbiddenErrorResponse extends BaseErrorResponse {
|
|
89
|
+
error: 'ForbiddenError';
|
|
90
|
+
statusCode: 403;
|
|
91
|
+
code: 'FORBIDDEN';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Unauthorized error response
|
|
95
|
+
*/
|
|
96
|
+
export interface UnauthorizedErrorResponse extends BaseErrorResponse {
|
|
97
|
+
error: 'UnauthorizedError';
|
|
98
|
+
statusCode: 401;
|
|
99
|
+
code: 'UNAUTHORIZED';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Service unavailable error response
|
|
103
|
+
*/
|
|
104
|
+
export interface ServiceUnavailableErrorResponse extends BaseErrorResponse {
|
|
105
|
+
error: 'ServiceUnavailableError';
|
|
106
|
+
statusCode: 503;
|
|
107
|
+
code: 'SERVICE_UNAVAILABLE';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Unprocessable entity error response
|
|
111
|
+
*/
|
|
112
|
+
export interface UnprocessableEntityErrorResponse extends BaseErrorResponse {
|
|
113
|
+
error: 'UnprocessableEntityError';
|
|
114
|
+
statusCode: 422;
|
|
115
|
+
code: 'UNPROCESSABLE';
|
|
116
|
+
}
|
|
67
117
|
/**
|
|
68
118
|
* Generic VeloxTS error response for all other errors
|
|
69
119
|
*/
|
|
@@ -86,7 +136,7 @@ export interface GenericErrorResponse extends BaseErrorResponse {
|
|
|
86
136
|
* }
|
|
87
137
|
* ```
|
|
88
138
|
*/
|
|
89
|
-
export type ErrorResponse = ValidationErrorResponse | NotFoundErrorResponse | GenericErrorResponse;
|
|
139
|
+
export type ErrorResponse = ValidationErrorResponse | NotFoundErrorResponse | ConflictErrorResponse | TooManyRequestsErrorResponse | ForbiddenErrorResponse | UnauthorizedErrorResponse | ServiceUnavailableErrorResponse | UnprocessableEntityErrorResponse | GenericErrorResponse;
|
|
90
140
|
/**
|
|
91
141
|
* Type guard for validation error responses
|
|
92
142
|
*/
|
|
@@ -272,6 +322,95 @@ export declare class NotFoundError extends VeloxError<'NOT_FOUND'> {
|
|
|
272
322
|
*/
|
|
273
323
|
toJSON(): NotFoundErrorResponse;
|
|
274
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Conflict error for duplicate resources
|
|
327
|
+
*
|
|
328
|
+
* Used when an operation would violate a uniqueness constraint
|
|
329
|
+
* (e.g., creating a user with an email that already exists)
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* throw new ConflictError('A user with this email already exists', ['email']);
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export declare class ConflictError extends VeloxError<'CONFLICT'> {
|
|
337
|
+
readonly fields?: string[];
|
|
338
|
+
constructor(message: string, fields?: string[]);
|
|
339
|
+
toJSON(): ConflictErrorResponse;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Forbidden error for insufficient permissions
|
|
343
|
+
*
|
|
344
|
+
* Used when an authenticated user lacks the required permissions
|
|
345
|
+
* for an operation (different from UnauthorizedError which means not authenticated)
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* throw new ForbiddenError('You do not have permission to delete this resource');
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
export declare class ForbiddenError extends VeloxError<'FORBIDDEN'> {
|
|
353
|
+
constructor(message?: string);
|
|
354
|
+
toJSON(): ForbiddenErrorResponse;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Unauthorized error for missing or invalid authentication
|
|
358
|
+
*
|
|
359
|
+
* Used when a request lacks valid authentication credentials
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* throw new UnauthorizedError('Invalid or expired token');
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
export declare class UnauthorizedError extends VeloxError<'UNAUTHORIZED'> {
|
|
367
|
+
constructor(message?: string);
|
|
368
|
+
toJSON(): UnauthorizedErrorResponse;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Service unavailable error for downstream failures
|
|
372
|
+
*
|
|
373
|
+
* Used when an external service or dependency is unreachable
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```typescript
|
|
377
|
+
* throw new ServiceUnavailableError('Payment gateway is temporarily unavailable');
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
export declare class ServiceUnavailableError extends VeloxError<'SERVICE_UNAVAILABLE'> {
|
|
381
|
+
constructor(message?: string);
|
|
382
|
+
toJSON(): ServiceUnavailableErrorResponse;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Too many requests error for rate limiting
|
|
386
|
+
*
|
|
387
|
+
* Used when a client exceeds the allowed request rate
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* throw new TooManyRequestsError('Rate limit exceeded', 60);
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
export declare class TooManyRequestsError extends VeloxError<'RATE_LIMITED'> {
|
|
395
|
+
readonly retryAfter?: number;
|
|
396
|
+
constructor(message?: string, retryAfter?: number);
|
|
397
|
+
toJSON(): TooManyRequestsErrorResponse;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Unprocessable entity error for semantically invalid requests
|
|
401
|
+
*
|
|
402
|
+
* Used when the request is syntactically valid but semantically wrong
|
|
403
|
+
* (e.g., trying to publish a draft that has no content)
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```typescript
|
|
407
|
+
* throw new UnprocessableEntityError('Cannot publish a post without content');
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export declare class UnprocessableEntityError extends VeloxError<'UNPROCESSABLE'> {
|
|
411
|
+
constructor(message: string);
|
|
412
|
+
toJSON(): UnprocessableEntityErrorResponse;
|
|
413
|
+
}
|
|
275
414
|
/**
|
|
276
415
|
* Type guard to check if an error is a VeloxError
|
|
277
416
|
*
|
|
@@ -311,6 +450,30 @@ export declare function isNotFoundError(error: unknown): error is NotFoundError;
|
|
|
311
450
|
* @returns true if error is a ConfigurationError instance
|
|
312
451
|
*/
|
|
313
452
|
export declare function isConfigurationError(error: unknown): error is ConfigurationError;
|
|
453
|
+
/**
|
|
454
|
+
* Type guard to check if an error is a ConflictError
|
|
455
|
+
*/
|
|
456
|
+
export declare function isConflictError(error: unknown): error is ConflictError;
|
|
457
|
+
/**
|
|
458
|
+
* Type guard to check if an error is a ForbiddenError
|
|
459
|
+
*/
|
|
460
|
+
export declare function isForbiddenError(error: unknown): error is ForbiddenError;
|
|
461
|
+
/**
|
|
462
|
+
* Type guard to check if an error is an UnauthorizedError
|
|
463
|
+
*/
|
|
464
|
+
export declare function isUnauthorizedError(error: unknown): error is UnauthorizedError;
|
|
465
|
+
/**
|
|
466
|
+
* Type guard to check if an error is a ServiceUnavailableError
|
|
467
|
+
*/
|
|
468
|
+
export declare function isServiceUnavailableError(error: unknown): error is ServiceUnavailableError;
|
|
469
|
+
/**
|
|
470
|
+
* Type guard to check if an error is a TooManyRequestsError
|
|
471
|
+
*/
|
|
472
|
+
export declare function isTooManyRequestsError(error: unknown): error is TooManyRequestsError;
|
|
473
|
+
/**
|
|
474
|
+
* Type guard to check if an error is an UnprocessableEntityError
|
|
475
|
+
*/
|
|
476
|
+
export declare function isUnprocessableEntityError(error: unknown): error is UnprocessableEntityError;
|
|
314
477
|
/**
|
|
315
478
|
* Helper to ensure exhaustive handling of error types
|
|
316
479
|
* Throws at compile time if a case is not handled
|
package/dist/errors.js
CHANGED
|
@@ -271,6 +271,177 @@ export class NotFoundError extends VeloxError {
|
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Conflict error for duplicate resources
|
|
276
|
+
*
|
|
277
|
+
* Used when an operation would violate a uniqueness constraint
|
|
278
|
+
* (e.g., creating a user with an email that already exists)
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```typescript
|
|
282
|
+
* throw new ConflictError('A user with this email already exists', ['email']);
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
export class ConflictError extends VeloxError {
|
|
286
|
+
fields;
|
|
287
|
+
constructor(message, fields) {
|
|
288
|
+
super(message, 409, 'CONFLICT');
|
|
289
|
+
this.name = 'ConflictError';
|
|
290
|
+
this.fields = fields;
|
|
291
|
+
if (Error.captureStackTrace) {
|
|
292
|
+
Error.captureStackTrace(this, ConflictError);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
toJSON() {
|
|
296
|
+
return {
|
|
297
|
+
error: 'ConflictError',
|
|
298
|
+
message: this.message,
|
|
299
|
+
statusCode: 409,
|
|
300
|
+
code: 'CONFLICT',
|
|
301
|
+
fields: this.fields,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Forbidden error for insufficient permissions
|
|
307
|
+
*
|
|
308
|
+
* Used when an authenticated user lacks the required permissions
|
|
309
|
+
* for an operation (different from UnauthorizedError which means not authenticated)
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* throw new ForbiddenError('You do not have permission to delete this resource');
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
export class ForbiddenError extends VeloxError {
|
|
317
|
+
constructor(message = 'Forbidden') {
|
|
318
|
+
super(message, 403, 'FORBIDDEN');
|
|
319
|
+
this.name = 'ForbiddenError';
|
|
320
|
+
if (Error.captureStackTrace) {
|
|
321
|
+
Error.captureStackTrace(this, ForbiddenError);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
toJSON() {
|
|
325
|
+
return {
|
|
326
|
+
error: 'ForbiddenError',
|
|
327
|
+
message: this.message,
|
|
328
|
+
statusCode: 403,
|
|
329
|
+
code: 'FORBIDDEN',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Unauthorized error for missing or invalid authentication
|
|
335
|
+
*
|
|
336
|
+
* Used when a request lacks valid authentication credentials
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* throw new UnauthorizedError('Invalid or expired token');
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export class UnauthorizedError extends VeloxError {
|
|
344
|
+
constructor(message = 'Unauthorized') {
|
|
345
|
+
super(message, 401, 'UNAUTHORIZED');
|
|
346
|
+
this.name = 'UnauthorizedError';
|
|
347
|
+
if (Error.captureStackTrace) {
|
|
348
|
+
Error.captureStackTrace(this, UnauthorizedError);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
toJSON() {
|
|
352
|
+
return {
|
|
353
|
+
error: 'UnauthorizedError',
|
|
354
|
+
message: this.message,
|
|
355
|
+
statusCode: 401,
|
|
356
|
+
code: 'UNAUTHORIZED',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Service unavailable error for downstream failures
|
|
362
|
+
*
|
|
363
|
+
* Used when an external service or dependency is unreachable
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* throw new ServiceUnavailableError('Payment gateway is temporarily unavailable');
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
export class ServiceUnavailableError extends VeloxError {
|
|
371
|
+
constructor(message = 'Service unavailable') {
|
|
372
|
+
super(message, 503, 'SERVICE_UNAVAILABLE');
|
|
373
|
+
this.name = 'ServiceUnavailableError';
|
|
374
|
+
if (Error.captureStackTrace) {
|
|
375
|
+
Error.captureStackTrace(this, ServiceUnavailableError);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
toJSON() {
|
|
379
|
+
return {
|
|
380
|
+
error: 'ServiceUnavailableError',
|
|
381
|
+
message: this.message,
|
|
382
|
+
statusCode: 503,
|
|
383
|
+
code: 'SERVICE_UNAVAILABLE',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Too many requests error for rate limiting
|
|
389
|
+
*
|
|
390
|
+
* Used when a client exceeds the allowed request rate
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```typescript
|
|
394
|
+
* throw new TooManyRequestsError('Rate limit exceeded', 60);
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
export class TooManyRequestsError extends VeloxError {
|
|
398
|
+
retryAfter;
|
|
399
|
+
constructor(message = 'Too many requests', retryAfter) {
|
|
400
|
+
super(message, 429, 'RATE_LIMITED');
|
|
401
|
+
this.name = 'TooManyRequestsError';
|
|
402
|
+
this.retryAfter = retryAfter;
|
|
403
|
+
if (Error.captureStackTrace) {
|
|
404
|
+
Error.captureStackTrace(this, TooManyRequestsError);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
toJSON() {
|
|
408
|
+
return {
|
|
409
|
+
error: 'TooManyRequestsError',
|
|
410
|
+
message: this.message,
|
|
411
|
+
statusCode: 429,
|
|
412
|
+
code: 'RATE_LIMITED',
|
|
413
|
+
retryAfter: this.retryAfter,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Unprocessable entity error for semantically invalid requests
|
|
419
|
+
*
|
|
420
|
+
* Used when the request is syntactically valid but semantically wrong
|
|
421
|
+
* (e.g., trying to publish a draft that has no content)
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```typescript
|
|
425
|
+
* throw new UnprocessableEntityError('Cannot publish a post without content');
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
export class UnprocessableEntityError extends VeloxError {
|
|
429
|
+
constructor(message) {
|
|
430
|
+
super(message, 422, 'UNPROCESSABLE');
|
|
431
|
+
this.name = 'UnprocessableEntityError';
|
|
432
|
+
if (Error.captureStackTrace) {
|
|
433
|
+
Error.captureStackTrace(this, UnprocessableEntityError);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
toJSON() {
|
|
437
|
+
return {
|
|
438
|
+
error: 'UnprocessableEntityError',
|
|
439
|
+
message: this.message,
|
|
440
|
+
statusCode: 422,
|
|
441
|
+
code: 'UNPROCESSABLE',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
274
445
|
// ============================================================================
|
|
275
446
|
// Type Guards
|
|
276
447
|
// ============================================================================
|
|
@@ -321,6 +492,42 @@ export function isNotFoundError(error) {
|
|
|
321
492
|
export function isConfigurationError(error) {
|
|
322
493
|
return error instanceof ConfigurationError;
|
|
323
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Type guard to check if an error is a ConflictError
|
|
497
|
+
*/
|
|
498
|
+
export function isConflictError(error) {
|
|
499
|
+
return error instanceof ConflictError;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Type guard to check if an error is a ForbiddenError
|
|
503
|
+
*/
|
|
504
|
+
export function isForbiddenError(error) {
|
|
505
|
+
return error instanceof ForbiddenError;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Type guard to check if an error is an UnauthorizedError
|
|
509
|
+
*/
|
|
510
|
+
export function isUnauthorizedError(error) {
|
|
511
|
+
return error instanceof UnauthorizedError;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Type guard to check if an error is a ServiceUnavailableError
|
|
515
|
+
*/
|
|
516
|
+
export function isServiceUnavailableError(error) {
|
|
517
|
+
return error instanceof ServiceUnavailableError;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Type guard to check if an error is a TooManyRequestsError
|
|
521
|
+
*/
|
|
522
|
+
export function isTooManyRequestsError(error) {
|
|
523
|
+
return error instanceof TooManyRequestsError;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Type guard to check if an error is an UnprocessableEntityError
|
|
527
|
+
*/
|
|
528
|
+
export function isUnprocessableEntityError(error) {
|
|
529
|
+
return error instanceof UnprocessableEntityError;
|
|
530
|
+
}
|
|
324
531
|
// ============================================================================
|
|
325
532
|
// Utility Types
|
|
326
533
|
// ============================================================================
|
package/dist/index.d.ts
CHANGED
|
@@ -21,16 +21,21 @@ export type { StartOptions } from './app.js';
|
|
|
21
21
|
export { VeloxApp, velox, veloxApp } from './app.js';
|
|
22
22
|
export type { BaseContext } from './context.js';
|
|
23
23
|
export { createContext, isContext, setupContextHook, setupTestContext } from './context.js';
|
|
24
|
-
export type { ErrorCode, ErrorResponse, GenericErrorResponse, InterpolationVars, NotFoundErrorResponse, ValidationErrorResponse, VeloxCoreErrorCode, VeloxErrorCode, VeloxErrorCodeRegistry, } from './errors.js';
|
|
25
|
-
export { assertNever, ConfigurationError, fail, isConfigurationError, isNotFoundError, isNotFoundErrorResponse, isValidationError, isValidationErrorResponse, isVeloxError, isVeloxFailure, logDeprecation, logWarning, NotFoundError, ValidationError, VeloxError, VeloxFailure, } from './errors.js';
|
|
26
|
-
export type { InferPluginOptions, PluginMetadata, PluginOptions, VeloxPlugin } from './plugin.js';
|
|
27
|
-
export { definePlugin, isFastifyPlugin, isVeloxPlugin, validatePluginMetadata } from './plugin.js';
|
|
24
|
+
export type { ConflictErrorResponse, ErrorCode, ErrorResponse, ForbiddenErrorResponse, GenericErrorResponse, InterpolationVars, NotFoundErrorResponse, ServiceUnavailableErrorResponse, TooManyRequestsErrorResponse, UnauthorizedErrorResponse, UnprocessableEntityErrorResponse, ValidationErrorResponse, VeloxCoreErrorCode, VeloxErrorCode, VeloxErrorCodeRegistry, } from './errors.js';
|
|
25
|
+
export { assertNever, ConfigurationError, ConflictError, ForbiddenError, fail, isConfigurationError, isConflictError, isForbiddenError, isNotFoundError, isNotFoundErrorResponse, isServiceUnavailableError, isTooManyRequestsError, isUnauthorizedError, isUnprocessableEntityError, isValidationError, isValidationErrorResponse, isVeloxError, isVeloxFailure, logDeprecation, logWarning, NotFoundError, ServiceUnavailableError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, ValidationError, VeloxError, VeloxFailure, } from './errors.js';
|
|
26
|
+
export type { ContextPluginConfig, InferPluginOptions, PluginMetadata, PluginOptions, VeloxPlugin, } from './plugin.js';
|
|
27
|
+
export { defineContextPlugin, definePlugin, isFastifyPlugin, isVeloxPlugin, validatePluginMetadata, } from './plugin.js';
|
|
28
28
|
export type { AsyncHandler, JsonArray, JsonObject, JsonPrimitive, JsonValue, LifecycleHook, ShutdownHandler, SyncHandler, } from './types.js';
|
|
29
29
|
export type { FrozenVeloxAppConfig, ValidHost, ValidPort, VeloxAppConfig, VeloxFastifyOptions, } from './utils/config.js';
|
|
30
30
|
export { isValidHost, isValidPort } from './utils/config.js';
|
|
31
31
|
export type { AuthContextExtension, CombineContexts, ContextExtension, CoreContext, DbContextExtension, defineContext, MergeContext, SessionContextExtension, } from './typed-context.js';
|
|
32
32
|
export type { CacheControl, StaticOptions } from './plugins/static.js';
|
|
33
33
|
export { registerStatic } from './plugins/static.js';
|
|
34
|
+
export { rawBodyPlugin } from './plugins/raw-body.js';
|
|
34
35
|
export { requestLogger } from './plugins/request-logger.js';
|
|
35
36
|
export type { Logger, LogLevel } from './utils/logger.js';
|
|
36
37
|
export { createLogger } from './utils/logger.js';
|
|
38
|
+
export type { FireAndForgetOptions } from './utils/fire-and-forget.js';
|
|
39
|
+
export { fireAndForget } from './utils/fire-and-forget.js';
|
|
40
|
+
export type { RetryOptions } from './utils/retry.js';
|
|
41
|
+
export { withRetry } from './utils/retry.js';
|
package/dist/index.js
CHANGED
|
@@ -21,12 +21,18 @@ const packageJson = require('../package.json');
|
|
|
21
21
|
export const VELOX_VERSION = packageJson.version ?? '0.0.0-unknown';
|
|
22
22
|
export { VeloxApp, velox, veloxApp } from './app.js';
|
|
23
23
|
export { createContext, isContext, setupContextHook, setupTestContext } from './context.js';
|
|
24
|
-
export { assertNever, ConfigurationError, fail, isConfigurationError, isNotFoundError, isNotFoundErrorResponse, isValidationError, isValidationErrorResponse, isVeloxError, isVeloxFailure, logDeprecation, logWarning, NotFoundError, ValidationError, VeloxError, VeloxFailure, } from './errors.js';
|
|
25
|
-
export { definePlugin, isFastifyPlugin, isVeloxPlugin, validatePluginMetadata } from './plugin.js';
|
|
24
|
+
export { assertNever, ConfigurationError, ConflictError, ForbiddenError, fail, isConfigurationError, isConflictError, isForbiddenError, isNotFoundError, isNotFoundErrorResponse, isServiceUnavailableError, isTooManyRequestsError, isUnauthorizedError, isUnprocessableEntityError, isValidationError, isValidationErrorResponse, isVeloxError, isVeloxFailure, logDeprecation, logWarning, NotFoundError, ServiceUnavailableError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, ValidationError, VeloxError, VeloxFailure, } from './errors.js';
|
|
25
|
+
export { defineContextPlugin, definePlugin, isFastifyPlugin, isVeloxPlugin, validatePluginMetadata, } from './plugin.js';
|
|
26
26
|
export { isValidHost, isValidPort } from './utils/config.js';
|
|
27
27
|
export { registerStatic } from './plugins/static.js';
|
|
28
28
|
// ============================================================================
|
|
29
|
+
// Raw Body (Webhook Support)
|
|
30
|
+
// ============================================================================
|
|
31
|
+
export { rawBodyPlugin } from './plugins/raw-body.js';
|
|
32
|
+
// ============================================================================
|
|
29
33
|
// Request Logging (Development)
|
|
30
34
|
// ============================================================================
|
|
31
35
|
export { requestLogger } from './plugins/request-logger.js';
|
|
32
36
|
export { createLogger } from './utils/logger.js';
|
|
37
|
+
export { fireAndForget } from './utils/fire-and-forget.js';
|
|
38
|
+
export { withRetry } from './utils/retry.js';
|
package/dist/plugin.d.ts
CHANGED
|
@@ -172,6 +172,58 @@ export declare function isVeloxPlugin(value: unknown): value is VeloxPlugin;
|
|
|
172
172
|
* ```
|
|
173
173
|
*/
|
|
174
174
|
export declare function isFastifyPlugin<Options extends PluginOptions = PluginOptions>(value: unknown): value is FastifyPluginAsync<Options>;
|
|
175
|
+
/**
|
|
176
|
+
* Configuration for a context-injecting plugin
|
|
177
|
+
*
|
|
178
|
+
* @template TService - The type of service to inject into the context
|
|
179
|
+
*/
|
|
180
|
+
export interface ContextPluginConfig<TService> {
|
|
181
|
+
/** Plugin name (e.g., '@myapp/analytics') */
|
|
182
|
+
name: string;
|
|
183
|
+
/** Plugin version (semver) */
|
|
184
|
+
version: string;
|
|
185
|
+
/** Context key used to access the service (e.g., 'analytics' for ctx.analytics) */
|
|
186
|
+
contextKey: string;
|
|
187
|
+
/** Factory function to create the service instance */
|
|
188
|
+
create: () => TService | Promise<TService>;
|
|
189
|
+
/** Optional cleanup function called on server shutdown */
|
|
190
|
+
close?: (service: TService) => void | Promise<void>;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Creates a plugin that injects a service into the request context
|
|
194
|
+
*
|
|
195
|
+
* Eliminates the runtime boilerplate of `Symbol.for()`, `decorateRequest()`,
|
|
196
|
+
* `addHook('onRequest')`, and `addHook('onClose')` that every
|
|
197
|
+
* context-injecting plugin must implement.
|
|
198
|
+
*
|
|
199
|
+
* **Note on types:** You still need `declare module` for TypeScript to
|
|
200
|
+
* know about the context property. This helper handles the runtime
|
|
201
|
+
* injection; declaration merging handles the types:
|
|
202
|
+
*
|
|
203
|
+
* ```typescript
|
|
204
|
+
* declare module '@veloxts/core' {
|
|
205
|
+
* interface BaseContext {
|
|
206
|
+
* analytics: Analytics;
|
|
207
|
+
* }
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @template TService - The type of service to inject
|
|
212
|
+
* @param config - Plugin configuration
|
|
213
|
+
* @returns A VeloxPlugin ready for registration
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* export const analyticsPlugin = defineContextPlugin({
|
|
218
|
+
* name: '@myapp/analytics',
|
|
219
|
+
* version: '1.0.0',
|
|
220
|
+
* contextKey: 'analytics',
|
|
221
|
+
* create: () => new Analytics(process.env.ANALYTICS_KEY!),
|
|
222
|
+
* close: (a) => a.flush(),
|
|
223
|
+
* });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export declare function defineContextPlugin<TService>(config: ContextPluginConfig<TService>): VeloxPlugin;
|
|
175
227
|
/**
|
|
176
228
|
* Infers the options type from a VeloxPlugin
|
|
177
229
|
*
|
package/dist/plugin.js
CHANGED
|
@@ -135,3 +135,66 @@ export function isVeloxPlugin(value) {
|
|
|
135
135
|
export function isFastifyPlugin(value) {
|
|
136
136
|
return typeof value === 'function';
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Creates a plugin that injects a service into the request context
|
|
140
|
+
*
|
|
141
|
+
* Eliminates the runtime boilerplate of `Symbol.for()`, `decorateRequest()`,
|
|
142
|
+
* `addHook('onRequest')`, and `addHook('onClose')` that every
|
|
143
|
+
* context-injecting plugin must implement.
|
|
144
|
+
*
|
|
145
|
+
* **Note on types:** You still need `declare module` for TypeScript to
|
|
146
|
+
* know about the context property. This helper handles the runtime
|
|
147
|
+
* injection; declaration merging handles the types:
|
|
148
|
+
*
|
|
149
|
+
* ```typescript
|
|
150
|
+
* declare module '@veloxts/core' {
|
|
151
|
+
* interface BaseContext {
|
|
152
|
+
* analytics: Analytics;
|
|
153
|
+
* }
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @template TService - The type of service to inject
|
|
158
|
+
* @param config - Plugin configuration
|
|
159
|
+
* @returns A VeloxPlugin ready for registration
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* export const analyticsPlugin = defineContextPlugin({
|
|
164
|
+
* name: '@myapp/analytics',
|
|
165
|
+
* version: '1.0.0',
|
|
166
|
+
* contextKey: 'analytics',
|
|
167
|
+
* create: () => new Analytics(process.env.ANALYTICS_KEY!),
|
|
168
|
+
* close: (a) => a.flush(),
|
|
169
|
+
* });
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export function defineContextPlugin(config) {
|
|
173
|
+
const symbolKey = Symbol.for(`${config.name}:${config.contextKey}`);
|
|
174
|
+
const plugin = definePlugin({
|
|
175
|
+
name: config.name,
|
|
176
|
+
version: config.version,
|
|
177
|
+
async register(fastify) {
|
|
178
|
+
const service = await config.create();
|
|
179
|
+
// Store on Fastify instance via symbol for programmatic retrieval
|
|
180
|
+
// (e.g., getCacheFromInstance pattern in ecosystem packages)
|
|
181
|
+
Object.defineProperty(fastify, symbolKey, {
|
|
182
|
+
value: service,
|
|
183
|
+
writable: false,
|
|
184
|
+
enumerable: false,
|
|
185
|
+
configurable: false,
|
|
186
|
+
});
|
|
187
|
+
fastify.decorateRequest(config.contextKey, undefined);
|
|
188
|
+
fastify.addHook('onRequest', async (request) => {
|
|
189
|
+
request[config.contextKey] = service;
|
|
190
|
+
});
|
|
191
|
+
if (config.close) {
|
|
192
|
+
const closeFn = config.close;
|
|
193
|
+
fastify.addHook('onClose', async () => {
|
|
194
|
+
await closeFn(service);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
return plugin;
|
|
200
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw Body Plugin
|
|
3
|
+
*
|
|
4
|
+
* Preserves the raw request body as a Buffer on the request object.
|
|
5
|
+
* Required for webhook signature verification (Stripe, GitHub, etc.)
|
|
6
|
+
* where the raw bytes must match the HMAC signature.
|
|
7
|
+
*
|
|
8
|
+
* Uses a `preParsing` hook to capture the raw bytes before Fastify's
|
|
9
|
+
* built-in JSON parser processes them, so normal JSON parsing is preserved
|
|
10
|
+
* for all routes.
|
|
11
|
+
*
|
|
12
|
+
* @module plugins/raw-body
|
|
13
|
+
*/
|
|
14
|
+
declare module 'fastify' {
|
|
15
|
+
interface FastifyRequest {
|
|
16
|
+
/** Raw request body as Buffer (available when rawBodyPlugin is registered) */
|
|
17
|
+
rawBody?: Buffer;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Fastify plugin that preserves the raw request body.
|
|
22
|
+
*
|
|
23
|
+
* After registration, `request.rawBody` contains the raw Buffer
|
|
24
|
+
* for all requests. Fastify's built-in JSON parser continues to
|
|
25
|
+
* work normally — this plugin does not replace it.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { rawBodyPlugin } from '@veloxts/core';
|
|
30
|
+
*
|
|
31
|
+
* await app.register(rawBodyPlugin);
|
|
32
|
+
*
|
|
33
|
+
* // In a webhook handler:
|
|
34
|
+
* const isValid = verifySignature(request.rawBody, request.headers['stripe-signature']);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const rawBodyPlugin: import("../plugin.js").VeloxPlugin<import("fastify").FastifyPluginOptions>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw Body Plugin
|
|
3
|
+
*
|
|
4
|
+
* Preserves the raw request body as a Buffer on the request object.
|
|
5
|
+
* Required for webhook signature verification (Stripe, GitHub, etc.)
|
|
6
|
+
* where the raw bytes must match the HMAC signature.
|
|
7
|
+
*
|
|
8
|
+
* Uses a `preParsing` hook to capture the raw bytes before Fastify's
|
|
9
|
+
* built-in JSON parser processes them, so normal JSON parsing is preserved
|
|
10
|
+
* for all routes.
|
|
11
|
+
*
|
|
12
|
+
* @module plugins/raw-body
|
|
13
|
+
*/
|
|
14
|
+
import { Readable } from 'node:stream';
|
|
15
|
+
import { definePlugin } from '../plugin.js';
|
|
16
|
+
/**
|
|
17
|
+
* Fastify plugin that preserves the raw request body.
|
|
18
|
+
*
|
|
19
|
+
* After registration, `request.rawBody` contains the raw Buffer
|
|
20
|
+
* for all requests. Fastify's built-in JSON parser continues to
|
|
21
|
+
* work normally — this plugin does not replace it.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { rawBodyPlugin } from '@veloxts/core';
|
|
26
|
+
*
|
|
27
|
+
* await app.register(rawBodyPlugin);
|
|
28
|
+
*
|
|
29
|
+
* // In a webhook handler:
|
|
30
|
+
* const isValid = verifySignature(request.rawBody, request.headers['stripe-signature']);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const rawBodyPlugin = definePlugin({
|
|
34
|
+
name: '@veloxts/raw-body',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
async register(fastify) {
|
|
37
|
+
fastify.decorateRequest('rawBody', undefined);
|
|
38
|
+
fastify.addHook('preParsing', async (request, _reply, payload) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
for await (const chunk of payload) {
|
|
41
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
42
|
+
}
|
|
43
|
+
const rawBody = Buffer.concat(chunks);
|
|
44
|
+
request.rawBody = rawBody;
|
|
45
|
+
return Readable.from(rawBody);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget utility for non-blocking side effects
|
|
3
|
+
*
|
|
4
|
+
* @module utils/fire-and-forget
|
|
5
|
+
*/
|
|
6
|
+
export interface FireAndForgetOptions {
|
|
7
|
+
/** Label for error logging context */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** Custom error handler (defaults to VeloxTS logger) */
|
|
10
|
+
onError?: (error: unknown) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Execute a promise without awaiting it, catching and logging any errors.
|
|
14
|
+
*
|
|
15
|
+
* Use this for non-critical side effects (analytics, notifications, audit logs)
|
|
16
|
+
* that shouldn't block the main response.
|
|
17
|
+
*
|
|
18
|
+
* @param promise - The promise to execute in the background
|
|
19
|
+
* @param options - Optional label and error handler
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { fireAndForget } from '@veloxts/core';
|
|
24
|
+
*
|
|
25
|
+
* // In a procedure handler
|
|
26
|
+
* fireAndForget(analytics.track('user.created', { userId }), {
|
|
27
|
+
* label: 'analytics',
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* return user; // Returns immediately
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function fireAndForget(promise: Promise<unknown>, options?: FireAndForgetOptions): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget utility for non-blocking side effects
|
|
3
|
+
*
|
|
4
|
+
* @module utils/fire-and-forget
|
|
5
|
+
*/
|
|
6
|
+
import { createLogger } from './logger.js';
|
|
7
|
+
const log = createLogger('core');
|
|
8
|
+
/**
|
|
9
|
+
* Execute a promise without awaiting it, catching and logging any errors.
|
|
10
|
+
*
|
|
11
|
+
* Use this for non-critical side effects (analytics, notifications, audit logs)
|
|
12
|
+
* that shouldn't block the main response.
|
|
13
|
+
*
|
|
14
|
+
* @param promise - The promise to execute in the background
|
|
15
|
+
* @param options - Optional label and error handler
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { fireAndForget } from '@veloxts/core';
|
|
20
|
+
*
|
|
21
|
+
* // In a procedure handler
|
|
22
|
+
* fireAndForget(analytics.track('user.created', { userId }), {
|
|
23
|
+
* label: 'analytics',
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* return user; // Returns immediately
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function fireAndForget(promise, options) {
|
|
30
|
+
const { label, onError } = options ?? {};
|
|
31
|
+
promise.catch((error) => {
|
|
32
|
+
if (onError) {
|
|
33
|
+
onError(error);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const prefix = label ? `[${label}] ` : '';
|
|
37
|
+
log.error(`${prefix}Fire-and-forget error:`, error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff
|
|
3
|
+
*
|
|
4
|
+
* @module utils/retry
|
|
5
|
+
*/
|
|
6
|
+
export interface RetryOptions {
|
|
7
|
+
/** Maximum number of attempts (default: 3, minimum: 1) */
|
|
8
|
+
attempts?: number;
|
|
9
|
+
/** Base delay in milliseconds (default: 500) */
|
|
10
|
+
delayMs?: number;
|
|
11
|
+
/** Use exponential backoff (default: true) */
|
|
12
|
+
exponential?: boolean;
|
|
13
|
+
/** Only retry if this predicate returns true */
|
|
14
|
+
retryIf?: (error: unknown) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Called after each failed attempt with the error and the 1-based
|
|
17
|
+
* attempt number that just failed. Called on every failure including
|
|
18
|
+
* the final one.
|
|
19
|
+
*/
|
|
20
|
+
onRetry?: (error: unknown, attempt: number) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retry an async operation with configurable backoff.
|
|
24
|
+
*
|
|
25
|
+
* @param fn - The async function to retry
|
|
26
|
+
* @param options - Retry configuration
|
|
27
|
+
* @returns The result of the first successful invocation
|
|
28
|
+
* @throws The last error if all attempts are exhausted
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { withRetry } from '@veloxts/core';
|
|
33
|
+
*
|
|
34
|
+
* const result = await withRetry(() => fetch('https://api.example.com/data'), {
|
|
35
|
+
* attempts: 3,
|
|
36
|
+
* retryIf: (err) => err instanceof Error && err.message.includes('ECONNRESET'),
|
|
37
|
+
* onRetry: (err, attempt) => console.log(`Attempt ${attempt} failed:`, err),
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff
|
|
3
|
+
*
|
|
4
|
+
* @module utils/retry
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Retry an async operation with configurable backoff.
|
|
8
|
+
*
|
|
9
|
+
* @param fn - The async function to retry
|
|
10
|
+
* @param options - Retry configuration
|
|
11
|
+
* @returns The result of the first successful invocation
|
|
12
|
+
* @throws The last error if all attempts are exhausted
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { withRetry } from '@veloxts/core';
|
|
17
|
+
*
|
|
18
|
+
* const result = await withRetry(() => fetch('https://api.example.com/data'), {
|
|
19
|
+
* attempts: 3,
|
|
20
|
+
* retryIf: (err) => err instanceof Error && err.message.includes('ECONNRESET'),
|
|
21
|
+
* onRetry: (err, attempt) => console.log(`Attempt ${attempt} failed:`, err),
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function withRetry(fn, options) {
|
|
26
|
+
const { attempts = 3, delayMs = 500, exponential = true, retryIf, onRetry } = options ?? {};
|
|
27
|
+
if (attempts < 1) {
|
|
28
|
+
throw new Error('withRetry: attempts must be at least 1');
|
|
29
|
+
}
|
|
30
|
+
let lastError;
|
|
31
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
32
|
+
try {
|
|
33
|
+
return await fn();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
lastError = error;
|
|
37
|
+
if (retryIf && !retryIf(error)) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
onRetry?.(error, attempt);
|
|
41
|
+
if (attempt < attempts) {
|
|
42
|
+
const delay = exponential ? delayMs * 2 ** (attempt - 1) : delayMs;
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw lastError;
|
|
48
|
+
}
|