unnbound-events 1.0.12 → 1.0.13

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.
@@ -1,4 +1,4 @@
1
1
  export { createEventsClient } from './lib/client';
2
- export type { EventsClient, EventRequest, EventResponse, RouteHandler, RouteMatcher, RegisteredRoute, SqsBatchEvent, } from './lib/types';
2
+ export type { EventsClient, EventRequest, EventResponse, RouteHandler, RouteMatcher, RegisteredRoute, SqsBatchEvent, StartOptions, } from './lib/types';
3
3
  export { createExpressMiddleware } from './lib/adapters/express';
4
4
  export { createSqsConsumer } from './lib/adapters/sqs';
@@ -183,16 +183,16 @@ function createEventsClient() {
183
183
  };
184
184
  },
185
185
  sqsListen(options) {
186
- const AWS = (() => {
186
+ const awsSqs = (() => {
187
187
  try {
188
- return require('aws-sdk');
188
+ return require('@aws-sdk/client-sqs');
189
189
  }
190
190
  catch {
191
191
  return null;
192
192
  }
193
193
  })();
194
- if (!AWS) {
195
- throw new Error('aws-sdk is required to use sqsListen');
194
+ if (!awsSqs) {
195
+ throw new Error('@aws-sdk/client-sqs is required to use sqsListen');
196
196
  }
197
197
  const env = globalThis
198
198
  .process?.env ?? {};
@@ -205,8 +205,7 @@ function createEventsClient() {
205
205
  const waitTimeSeconds = options?.waitTimeSeconds ?? 10;
206
206
  const maxMessages = options?.maxMessages ?? 10;
207
207
  const visibilityTimeout = options?.visibilityTimeoutSeconds;
208
- const AWSLib = AWS;
209
- const sqs = new AWSLib.SQS({ region, endpoint });
208
+ const sqs = new awsSqs.SQSClient({ region, endpoint });
210
209
  let stopped = false;
211
210
  const consume = this.sqs();
212
211
  async function loop() {
@@ -216,10 +215,9 @@ function createEventsClient() {
216
215
  QueueUrl: queueUrl,
217
216
  MaxNumberOfMessages: maxMessages,
218
217
  WaitTimeSeconds: waitTimeSeconds,
218
+ ...(visibilityTimeout && { VisibilityTimeout: visibilityTimeout }),
219
219
  };
220
- if (visibilityTimeout)
221
- params.VisibilityTimeout = visibilityTimeout;
222
- const resp = await sqs.receiveMessage(params).promise();
220
+ const resp = (await sqs.send(new awsSqs.ReceiveMessageCommand(params)));
223
221
  const messages = (resp.Messages ?? []).map((m) => ({
224
222
  id: m.MessageId ?? '',
225
223
  body: String(m.Body ?? ''),
@@ -237,7 +235,7 @@ function createEventsClient() {
237
235
  for (let i = 0; i < toDelete.length; i += 10) {
238
236
  const chunk = toDelete.slice(i, i + 10);
239
237
  if (chunk.length > 0) {
240
- await sqs.deleteMessageBatch({ QueueUrl: queueUrl, Entries: chunk }).promise();
238
+ await sqs.send(new awsSqs.DeleteMessageBatchCommand({ QueueUrl: queueUrl, Entries: chunk }));
241
239
  }
242
240
  }
243
241
  }
@@ -256,5 +254,59 @@ function createEventsClient() {
256
254
  },
257
255
  });
258
256
  },
257
+ async start(options) {
258
+ // Default to starting both HTTP and SQS if no options provided
259
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
260
+ const httpOptions = options?.http ?? { port: 3000 };
261
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
262
+ const sqsOptions = options?.sqs ?? {};
263
+ // Start HTTP server
264
+ let httpServer;
265
+ try {
266
+ httpServer = await this.http(httpOptions);
267
+ }
268
+ catch (error) {
269
+ unnbound_logger_sdk_1.logger.warn({ error }, 'Failed to start HTTP server, continuing without it');
270
+ }
271
+ // Start SQS listener
272
+ let sqsListener;
273
+ try {
274
+ sqsListener = await this.sqsListen(sqsOptions);
275
+ }
276
+ catch (error) {
277
+ unnbound_logger_sdk_1.logger.warn({ error }, 'Failed to start SQS listener, continuing without it');
278
+ }
279
+ // If neither HTTP nor SQS started successfully, throw an error
280
+ if (!httpServer && !sqsListener) {
281
+ throw new Error('Failed to start any transport. Please check your configuration and try again.');
282
+ }
283
+ // Set up graceful shutdown
284
+ const g = globalThis;
285
+ const shutdown = async () => {
286
+ unnbound_logger_sdk_1.logger.info('Received shutdown signal, closing servers...');
287
+ const promises = [];
288
+ if (httpServer)
289
+ promises.push(httpServer.close());
290
+ if (sqsListener)
291
+ promises.push(sqsListener.stop());
292
+ await Promise.all(promises);
293
+ unnbound_logger_sdk_1.logger.info('All servers closed');
294
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
295
+ g.process?.exit?.(0);
296
+ };
297
+ // Handle SIGINT and SIGTERM
298
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
299
+ g.process?.on?.('SIGINT', shutdown);
300
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
301
+ g.process?.on?.('SIGTERM', shutdown);
302
+ // Log startup information
303
+ const services = [];
304
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
305
+ if (httpServer)
306
+ services.push(`HTTP on port ${httpOptions?.port ?? 3000}`);
307
+ if (sqsListener)
308
+ services.push('SQS listener');
309
+ unnbound_logger_sdk_1.logger.info({ services: services.join(' and ') }, 'Services started. Press Ctrl+C to stop.');
310
+ },
259
311
  };
260
312
  }
@@ -26,6 +26,18 @@ export type SqsRecord = {
26
26
  export interface SqsBatchEvent {
27
27
  Records: SqsRecord[];
28
28
  }
29
+ export interface StartOptions {
30
+ http?: {
31
+ port?: number;
32
+ };
33
+ sqs?: {
34
+ queueUrl?: string;
35
+ region?: string;
36
+ waitTimeSeconds?: number;
37
+ maxMessages?: number;
38
+ visibilityTimeoutSeconds?: number;
39
+ };
40
+ }
29
41
  export interface EventsClient {
30
42
  on: (method: HttpMethod, matcher: RouteMatcher, handler: RouteHandler<any, any>) => void;
31
43
  handle: (request: EventRequest<any>) => Promise<EventResponse<any>>;
@@ -57,4 +69,5 @@ export interface EventsClient {
57
69
  }) => Promise<{
58
70
  stop: () => Promise<void>;
59
71
  }>;
72
+ start: (options?: StartOptions) => Promise<void>;
60
73
  }
@@ -24,6 +24,7 @@ function isMiddleware(value) {
24
24
  return typeof value === 'function' && value.length >= 2;
25
25
  }
26
26
  const TRACE_HEADER_KEY = 'x-unnbound-trace-id';
27
+ const MESSAGE_HEADER_KEY = 'x-message-id';
27
28
  const getHeaderValue = (headers, name) => {
28
29
  const needle = name.toLowerCase();
29
30
  for (const [key, value] of Object.entries(headers)) {
@@ -54,21 +55,26 @@ function createEventsClient(ignoreTraceRoutes = exports.defaultIgnoreTraceRoutes
54
55
  }
55
56
  const metadataTraceId = typeof request.metadata?.traceId === 'string' ? request.metadata.traceId : undefined;
56
57
  const metadataMessageId = typeof request.metadata?.messageId === 'string' ? request.metadata.messageId : undefined;
58
+ const headerMessageId = getHeaderValue(request.headers, MESSAGE_HEADER_KEY);
59
+ if (metadataMessageId && headerMessageId && metadataMessageId !== headerMessageId) {
60
+ unnbound_logger_sdk_1.logger.warn({ metadataMessageId, headerMessageId }, 'Message ID mismatch detected; defaulting to header value.');
61
+ }
62
+ const messageId = headerMessageId ?? metadataMessageId;
57
63
  const headerTraceId = getHeaderValue(request.headers, TRACE_HEADER_KEY);
58
64
  const traceId = metadataTraceId ?? headerTraceId;
59
- const context = traceId || metadataMessageId
65
+ const context = traceId || messageId
60
66
  ? {
61
67
  ...(traceId ? { traceId } : {}),
62
- ...(metadataMessageId ? { messageId: metadataMessageId } : {}),
68
+ ...(messageId ? { messageId } : {}),
63
69
  }
64
70
  : undefined;
65
- const enrichedRequest = traceId || metadataMessageId
71
+ const enrichedRequest = traceId || messageId
66
72
  ? {
67
73
  ...request,
68
74
  metadata: {
69
75
  ...request.metadata,
70
76
  ...(traceId ? { traceId } : {}),
71
- ...(metadataMessageId ? { messageId: metadataMessageId } : {}),
77
+ ...(messageId ? { messageId } : {}),
72
78
  },
73
79
  }
74
80
  : request;
@@ -248,12 +254,27 @@ function createEventsClient(ignoreTraceRoutes = exports.defaultIgnoreTraceRoutes
248
254
  await Promise.all(event.Records.map(async (record) => {
249
255
  try {
250
256
  const envelope = JSON.parse(record.body);
251
- const messageId = record.messageId;
257
+ const headerMessageId = getHeaderValue(envelope.request.headers, MESSAGE_HEADER_KEY);
258
+ const envelopeMessageId = typeof envelope.metadata?.messageId === 'string'
259
+ ? envelope.metadata.messageId
260
+ : typeof envelope.messageId === 'string'
261
+ ? envelope.messageId
262
+ : undefined;
263
+ if (envelopeMessageId && headerMessageId && envelopeMessageId !== headerMessageId) {
264
+ unnbound_logger_sdk_1.logger.warn({ envelopeMessageId, headerMessageId }, 'Message ID mismatch detected in SQS envelope; defaulting to header value.');
265
+ }
266
+ const messageId = headerMessageId ?? envelopeMessageId ?? record.messageId ?? undefined;
267
+ const originalMessageId = envelopeMessageId && envelopeMessageId !== messageId
268
+ ? envelopeMessageId
269
+ : undefined;
252
270
  const originalTraceId = typeof envelope.metadata?.traceId === 'string'
253
271
  ? envelope.metadata.traceId
254
272
  : undefined;
255
273
  const traceId = (0, unnbound_logger_sdk_1.getTraceId)();
256
- const traceContext = { traceId, messageId };
274
+ const traceContext = {
275
+ traceId,
276
+ ...(messageId ? { messageId } : {}),
277
+ };
257
278
  await (0, unnbound_logger_sdk_1.withTrace)(async () => {
258
279
  const req = {
259
280
  method: envelope.request.method,
@@ -269,7 +290,11 @@ function createEventsClient(ignoreTraceRoutes = exports.defaultIgnoreTraceRoutes
269
290
  timestamp: envelope.timestamp,
270
291
  version: envelope.version,
271
292
  traceId,
272
- messageId,
293
+ ...(messageId ? { messageId } : {}),
294
+ ...(originalMessageId ? { envelopeMessageId: originalMessageId } : {}),
295
+ ...(record.messageId && record.messageId !== messageId
296
+ ? { deliveryMessageId: record.messageId }
297
+ : {}),
273
298
  ...(originalTraceId && originalTraceId !== traceId ? { originalTraceId } : {}),
274
299
  },
275
300
  };
@@ -1,9 +1,11 @@
1
- import { Axios } from 'axios';
1
+ import { Axios, type AxiosRequestConfig, type AxiosResponse } from 'axios';
2
2
  import { HttpOptions } from './types';
3
+ type GetPayload = (config: AxiosRequestConfig, res?: AxiosResponse) => object;
3
4
  /**
4
5
  * Wraps an axios instance to add tracing and span tracking
5
6
  * @param axios - The axios instance to wrap
6
7
  * @param options - Configuration options for HTTP tracing
7
8
  * @returns The wrapped axios instance with span tracking
8
9
  */
9
- export declare const traceAxios: (client: Axios, { ignoreTraceRoutes, traceHeaderKey, }?: HttpOptions) => Axios;
10
+ export declare const traceAxios: (client: Axios, { ignoreTraceRoutes, traceHeaderKey, getPayload, }?: HttpOptions<GetPayload>) => Axios;
11
+ export {};
@@ -25,15 +25,17 @@ const buildOutgoingHttpPayload = (config, res) => ({
25
25
  : undefined,
26
26
  },
27
27
  });
28
+ const getNoopPayload = () => ({});
28
29
  /**
29
30
  * Wraps an axios instance to add tracing and span tracking
30
31
  * @param axios - The axios instance to wrap
31
32
  * @param options - Configuration options for HTTP tracing
32
33
  * @returns The wrapped axios instance with span tracking
33
34
  */
34
- const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes, traceHeaderKey = types_1.defaultTraceHeaderKey, } = {
35
+ const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes, traceHeaderKey = types_1.defaultTraceHeaderKey, getPayload = getNoopPayload, } = {
35
36
  ignoreTraceRoutes: types_1.defaultIgnoreTraceRoutes,
36
37
  traceHeaderKey: types_1.defaultTraceHeaderKey,
38
+ getPayload: getNoopPayload,
37
39
  }) => {
38
40
  const createSpanWrappedRequest = (originalMethod, method) => {
39
41
  const { headers: defaultHeaders, ...partialDefaultConfig } = client.defaults;
@@ -50,11 +52,14 @@ const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRout
50
52
  config = { ...config, headers: { ...config.headers, [traceHeaderKey]: traceId } };
51
53
  // Execute the request within a span
52
54
  return (0, span_1.startSpan)(`Outgoing HTTP request`, () => originalMethod(config), (options) => {
53
- if (!options)
54
- return buildOutgoingHttpPayload(config);
55
- if (options.error)
56
- return buildOutgoingHttpPayload(config, (0, axios_1.isAxiosError)(options.error) ? options.error.response : undefined);
57
- return buildOutgoingHttpPayload(config, options.result);
55
+ const response = options
56
+ ? options.error
57
+ ? (0, axios_1.isAxiosError)(options.error)
58
+ ? options.error.response
59
+ : undefined
60
+ : options.result
61
+ : undefined;
62
+ return { ...buildOutgoingHttpPayload(config, response), ...getPayload(config, response) };
58
63
  });
59
64
  };
60
65
  };
@@ -1,3 +1,10 @@
1
- import { RequestHandler } from 'express';
1
+ import { Request, RequestHandler } from 'express';
2
2
  import { HttpOptions } from './types';
3
- export declare const traceMiddleware: ({ ignoreTraceRoutes, traceHeaderKey, }?: HttpOptions) => RequestHandler;
3
+ interface Response {
4
+ status: number;
5
+ headers?: Record<string, string | undefined>;
6
+ body?: unknown;
7
+ }
8
+ type GetPayload = (config: Request, res?: Response) => object;
9
+ export declare const traceMiddleware: ({ ignoreTraceRoutes, traceHeaderKey, getPayload, }?: HttpOptions<GetPayload>) => RequestHandler;
10
+ export {};
@@ -19,7 +19,7 @@ const getFullUrl = (req) => {
19
19
  const host = req.get('host') || req.get('x-forwarded-host') || 'localhost';
20
20
  return `${protocol}://${host}${url}`;
21
21
  };
22
- const buildIncomingHttpPayload = (req, res, body) => ({
22
+ const buildIncomingHttpPayload = (req, res) => ({
23
23
  type: 'http',
24
24
  http: {
25
25
  url: getFullUrl(req),
@@ -32,16 +32,18 @@ const buildIncomingHttpPayload = (req, res, body) => ({
32
32
  },
33
33
  response: res
34
34
  ? {
35
- headers: res?.headers,
36
- status: res.statusCode,
37
- body: (0, utils_1.safeJsonParse)(body),
35
+ headers: res.headers,
36
+ status: res.status,
37
+ body: (0, utils_1.safeJsonParse)(res.body),
38
38
  }
39
39
  : undefined,
40
40
  },
41
41
  });
42
- const traceMiddleware = ({ ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes, traceHeaderKey = types_1.defaultTraceHeaderKey, } = {
42
+ const getNoopPayload = () => ({});
43
+ const traceMiddleware = ({ ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes, traceHeaderKey = types_1.defaultTraceHeaderKey, getPayload = getNoopPayload, } = {
43
44
  ignoreTraceRoutes: types_1.defaultIgnoreTraceRoutes,
44
45
  traceHeaderKey: types_1.defaultTraceHeaderKey,
46
+ getPayload: getNoopPayload,
45
47
  }) => async (req, res, next) => {
46
48
  if ((0, utils_1.shouldIgnorePath)(req.path, ignoreTraceRoutes))
47
49
  return next();
@@ -60,12 +62,14 @@ const traceMiddleware = ({ ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes,
60
62
  return next();
61
63
  });
62
64
  }, (o) => {
63
- return buildIncomingHttpPayload(req, o
65
+ const response = o
64
66
  ? {
65
- statusCode: res.statusCode,
67
+ status: res.statusCode,
66
68
  headers: res.getHeaders(),
69
+ body: res.locals.body,
67
70
  }
68
- : undefined, res.locals.body);
71
+ : undefined;
72
+ return { ...buildIncomingHttpPayload(req, response), ...getPayload(req, response) };
69
73
  });
70
74
  }, { traceId });
71
75
  };
@@ -53,9 +53,10 @@ export interface SftpPayload {
53
53
  content?: string;
54
54
  exists?: string | false;
55
55
  }
56
- export interface HttpOptions {
56
+ export interface HttpOptions<G extends Function> {
57
57
  ignoreTraceRoutes?: string[];
58
58
  traceHeaderKey?: string;
59
+ getPayload?: G;
59
60
  }
60
61
  export declare const defaultIgnoreTraceRoutes: string[];
61
62
  export declare const defaultTraceHeaderKey = "x-unnbound-trace-id";
package/package.json CHANGED
@@ -1,9 +1,25 @@
1
1
  {
2
2
  "name": "unnbound-events",
3
3
  "description": "Unified events SDK to handle HTTP routes and SQS messages with a single routing API.",
4
- "version": "1.0.12",
4
+ "version": "1.0.13",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "test:coverage": "jest --coverage",
11
+ "test:watch": "jest --watch",
12
+ "typecheck": "tsc -noEmit",
13
+ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
14
+ "lint:fix": "pnpm lint --fix",
15
+ "format": "prettier --write .",
16
+ "format:check": "prettier --check .",
17
+ "prepublishOnly": "pnpm build",
18
+ "start:example:express": "npx --yes tsx --watch examples/express.ts",
19
+ "start:example:sqs": "npx --yes tsx --watch examples/sqs.ts",
20
+ "start:example:http-and-sqs": "npx --yes tsx --watch examples/http-and-sqs.ts",
21
+ "version:bump": "npm version patch"
22
+ },
7
23
  "author": "Unnbound Team",
8
24
  "license": "MIT",
9
25
  "repository": {
@@ -17,7 +33,7 @@
17
33
  "dependencies": {
18
34
  "express": "^4.0.0 || ^5.0.0",
19
35
  "@aws-sdk/client-sqs": "^3.0.0",
20
- "unnbound-logger-sdk": "3.0.19"
36
+ "unnbound-logger-sdk": "workspace:*"
21
37
  },
22
38
  "peerDependencies": {
23
39
  "express": "^4.0.0 || ^5.0.0"
@@ -40,20 +56,5 @@
40
56
  "engines": {
41
57
  "node": ">=22"
42
58
  },
43
- "sideEffects": false,
44
- "scripts": {
45
- "build": "tsc",
46
- "test": "jest",
47
- "test:coverage": "jest --coverage",
48
- "test:watch": "jest --watch",
49
- "typecheck": "tsc -noEmit",
50
- "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
51
- "lint:fix": "pnpm lint --fix",
52
- "format": "prettier --write .",
53
- "format:check": "prettier --check .",
54
- "start:example:express": "npx --yes tsx --watch examples/express.ts",
55
- "start:example:sqs": "npx --yes tsx --watch examples/sqs.ts",
56
- "start:example:http-and-sqs": "npx --yes tsx --watch examples/http-and-sqs.ts",
57
- "version:bump": "npm version patch"
58
- }
59
- }
59
+ "sideEffects": false
60
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Unnbound Team
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.