unnbound-events 2.0.13 → 2.0.15

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,11 +1,16 @@
1
1
  import { S3Client } from '@aws-sdk/client-s3';
2
- import { ReceiveMessageCommand, ReceiveMessageCommandOutput, SQSClient } from '@aws-sdk/client-sqs';
2
+ import { Message, ReceiveMessageCommand, SQSClient } from '@aws-sdk/client-sqs';
3
+ import { IncomingEvent, IncomingRequest } from '../event';
3
4
  export interface S3Pointer {
4
5
  bucket: string;
5
6
  key: string;
6
7
  }
7
8
  export type S3PointerMessage = ['software.amazon.payloadoffloading.PayloadS3Pointer', S3Pointer];
8
9
  type SQSClientConfig = ConstructorParameters<typeof SQSClient>[0];
10
+ export interface ExtendedSqsMessage {
11
+ message: Message;
12
+ event: IncomingEvent<IncomingRequest>;
13
+ }
9
14
  export type ExtendedSQSClientConfig = SQSClientConfig & {
10
15
  s3: S3Client;
11
16
  };
@@ -13,8 +18,10 @@ export declare class ExtendedSQSClient extends SQSClient {
13
18
  private readonly s3;
14
19
  private readonly S3_POINTER_CLASS;
15
20
  constructor(config: ExtendedSQSClientConfig);
16
- private isS3PointerMessage;
21
+ receive(command: ReceiveMessageCommand, options?: Parameters<SQSClient['send']>[1]): Promise<ExtendedSqsMessage[]>;
17
22
  private retrieveMessage;
18
- receive(command: ReceiveMessageCommand, options?: Parameters<SQSClient['send']>[1]): Promise<ReceiveMessageCommandOutput>;
23
+ private isSqsMessage;
24
+ private isS3PointerMessage;
25
+ private parseEvent;
19
26
  }
20
27
  export {};
@@ -5,6 +5,7 @@ const client_s3_1 = require("@aws-sdk/client-s3");
5
5
  const client_sqs_1 = require("@aws-sdk/client-sqs");
6
6
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
7
7
  const internal_1 = require("unnbound-logger-sdk/dist/internal");
8
+ const utils_1 = require("../utils");
8
9
  class ExtendedSQSClient extends client_sqs_1.SQSClient {
9
10
  s3;
10
11
  S3_POINTER_CLASS = 'software.amazon.payloadoffloading.PayloadS3Pointer';
@@ -12,32 +13,60 @@ class ExtendedSQSClient extends client_sqs_1.SQSClient {
12
13
  super(config);
13
14
  this.s3 = config.s3;
14
15
  }
15
- isS3PointerMessage(body) {
16
- return Array.isArray(body) && body.length === 2 && body[0] === this.S3_POINTER_CLASS;
16
+ async receive(command, options) {
17
+ const result = await this.send(command, options);
18
+ return await Promise.all(result.Messages?.map(async (message) => ({
19
+ message,
20
+ event: await this.retrieveMessage(message),
21
+ })) ?? []);
17
22
  }
18
23
  async retrieveMessage(message) {
19
24
  if (!message.Body)
20
- return message;
21
- if (!message.Body.startsWith(`["${this.S3_POINTER_CLASS}",`))
22
- return message;
25
+ throw new Error('Queue message body is missing.');
23
26
  const parsed = JSON.parse(message.Body);
24
- if (!this.isS3PointerMessage(parsed))
25
- return message;
26
- const { bucket, key } = parsed[1];
27
+ if (!this.isSqsMessage(parsed))
28
+ throw new Error('Invalid queue message format.');
29
+ const payload = parsed.request.body;
30
+ if (!this.isS3PointerMessage(payload))
31
+ return this.parseEvent(parsed);
32
+ const { bucket, key } = payload[1];
27
33
  const body = await (0, unnbound_logger_sdk_1.startSpan)('Retrieve message from storage', async () => {
28
34
  const payload = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }));
29
- const body = await payload.Body?.transformToString();
35
+ const body = await payload.Body?.transformToByteArray();
30
36
  if (!body)
31
37
  throw new Error('Failed to retrieve queue message from storage.');
32
- return body;
33
- }, (o) => ({ bucket, key, ...(0, internal_1.internal)(), size: o?.result?.length }));
34
- return { ...message, Body: body };
38
+ return body.buffer;
39
+ }, (o) => ({ bucket, key, ...(0, internal_1.internal)(), size: o?.result?.byteLength }));
40
+ return this.parseEvent({ ...parsed, request: { ...parsed.request, body } });
35
41
  }
36
- async receive(command, options) {
37
- const result = await this.send(command, options);
42
+ isSqsMessage(message) {
43
+ return (!!message &&
44
+ typeof message === 'object' &&
45
+ 'id' in message &&
46
+ 'timestamp' in message &&
47
+ 'request' in message);
48
+ }
49
+ isS3PointerMessage(body) {
50
+ return Array.isArray(body) && body.length === 2 && body[0] === this.S3_POINTER_CLASS;
51
+ }
52
+ parseEvent(message) {
53
+ const method = message.request.method.toLowerCase();
54
+ const url = new URL(message.request.url, `https://unnbound.ai`);
55
+ const request = {
56
+ url: url.toString(),
57
+ method,
58
+ path: url.pathname,
59
+ headers: (0, utils_1.normalizeHeaders)(message.request.headers),
60
+ query: Object.fromEntries(url.searchParams.entries()),
61
+ body: message.request.body,
62
+ };
38
63
  return {
39
- ...result,
40
- Messages: await Promise.all(result.Messages?.map(this.retrieveMessage.bind(this)) ?? []),
64
+ request,
65
+ timestamp: message.timestamp,
66
+ metadata: {
67
+ ...message.metadata,
68
+ ...(0, utils_1.getMetadataFromRequest)(request, 'queue'),
69
+ },
41
70
  };
42
71
  }
43
72
  }
@@ -34,6 +34,4 @@ export declare class QueueAdapter {
34
34
  private processMessage;
35
35
  private getReceiveCommand;
36
36
  private delete;
37
- private isSqsMessage;
38
- private parseEvent;
39
37
  }
@@ -41,10 +41,10 @@ class QueueAdapter {
41
41
  if (controller.signal.aborted)
42
42
  return resolve(void 0);
43
43
  try {
44
- const { Messages: messages } = await this.sqs.receive(this.getReceiveCommand(false), {
44
+ const messages = await this.sqs.receive(this.getReceiveCommand(false), {
45
45
  abortSignal: controller.signal,
46
46
  });
47
- if (!messages?.length)
47
+ if (!messages.length)
48
48
  return;
49
49
  const { failed } = await (0, unnbound_logger_sdk_1.startSpan)('Process messages', async () => {
50
50
  const results = await Promise.all(messages.map(this.processMessage.bind(this)));
@@ -77,14 +77,17 @@ class QueueAdapter {
77
77
  }
78
78
  };
79
79
  }
80
- async processMessage(message) {
80
+ async processMessage({ message, event, }) {
81
81
  try {
82
- const event = this.parseEvent(message);
83
82
  // It forward the request to the HTTP server that will
84
83
  // handle it according to the routes the user set up.
85
84
  const response = await this.app.request(event.request.url, {
86
85
  method: event.request.method,
87
- body: event.request.body,
86
+ body: event.request.body instanceof ArrayBuffer || event.request.body instanceof FormData
87
+ ? event.request.body
88
+ : typeof event.request.body === 'object'
89
+ ? JSON.stringify(event.request.body)
90
+ : event.request.body,
88
91
  headers: {
89
92
  ...event.request.headers,
90
93
  ...(0, utils_1.serializeMetadata)({ source: 'queue', timestamp: event.timestamp }),
@@ -118,44 +121,15 @@ class QueueAdapter {
118
121
  async delete(messages) {
119
122
  if (!messages.length)
120
123
  return;
121
- await this.sqs.send(new client_sqs_1.DeleteMessageBatchCommand({
122
- QueueUrl: this.getQueue(),
123
- Entries: messages.map((m) => ({ Id: m.MessageId, ReceiptHandle: m.ReceiptHandle })),
124
+ const queue = this.getQueue();
125
+ // Batch the messages into 10 messages per batch
126
+ const batches = (0, utils_1.batchArray)(messages, 10);
127
+ await Promise.all(batches.map(async (messages) => {
128
+ await this.sqs.send(new client_sqs_1.DeleteMessageBatchCommand({
129
+ QueueUrl: queue,
130
+ Entries: messages.map((m) => ({ Id: m.MessageId, ReceiptHandle: m.ReceiptHandle })),
131
+ }));
124
132
  }));
125
133
  }
126
- isSqsMessage(message) {
127
- return (!!message &&
128
- typeof message === 'object' &&
129
- 'id' in message &&
130
- 'timestamp' in message &&
131
- 'request' in message);
132
- }
133
- parseEvent(message) {
134
- if (!message.Body)
135
- throw new Error('Queue message body is missing.');
136
- if (typeof message.Body !== 'string')
137
- throw new Error('Queue message body is malformed.');
138
- const parsed = JSON.parse(message.Body);
139
- if (!this.isSqsMessage(parsed))
140
- throw new Error('Invalid queue message format.');
141
- const method = parsed.request.method.toLowerCase();
142
- const url = new URL(parsed.request.url, `https://unnbound.ai`);
143
- const request = {
144
- url: url.toString(),
145
- method,
146
- path: url.pathname,
147
- headers: (0, utils_1.normalizeHeaders)(parsed.request.headers),
148
- query: Object.fromEntries(url.searchParams.entries()),
149
- body: parsed.request.body ? Buffer.from(parsed.request.body, 'base64') : undefined,
150
- };
151
- return {
152
- request,
153
- timestamp: parsed.timestamp,
154
- metadata: {
155
- ...parsed.metadata,
156
- ...(0, utils_1.getMetadataFromRequest)(request, 'queue'),
157
- },
158
- };
159
- }
160
134
  }
161
135
  exports.QueueAdapter = QueueAdapter;
@@ -21,6 +21,8 @@ exports.parseEndpointArguments = parseEndpointArguments;
21
21
  const patchEndpoint = (handler) => {
22
22
  return async (context) => {
23
23
  const state = context.get('state');
24
+ // We have to recalculate the params because the handler path might contain url params
25
+ state.request.params = { ...state.request.params, ...context.req.param() };
24
26
  const result = await (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => {
25
27
  const result = await handler(state);
26
28
  return { status: result?.status ?? 204, headers: result?.headers, body: result?.body };
@@ -2,7 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildMiddlewareArguments = exports.parseMiddlewareArguments = exports.patchMiddleware = void 0;
4
4
  const patchMiddleware = (handler) => {
5
- return (context, next) => handler(context.get('state'), next);
5
+ return (context, next) => {
6
+ const state = context.get('state');
7
+ // We have to recalculate the params because middleware path might contain url params
8
+ state.request.params = { ...state.request.params, ...context.req.param() };
9
+ return handler(state, next);
10
+ };
6
11
  };
7
12
  exports.patchMiddleware = patchMiddleware;
8
13
  const parseMiddlewareArguments = (...args) => {
@@ -30,4 +30,5 @@ export declare const buildIncomingPayload: (event: IncomingEvent<MatchedIncoming
30
30
  };
31
31
  };
32
32
  };
33
+ export declare const batchArray: <T>(array: T[], size: number) => T[][];
33
34
  export {};
package/dist/lib/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildIncomingPayload = exports.serializeMetadata = exports.getMetadataFromRequest = exports.getIncomingEvent = exports.normalizeHeaders = void 0;
3
+ exports.batchArray = exports.buildIncomingPayload = exports.serializeMetadata = exports.getMetadataFromRequest = exports.getIncomingEvent = exports.normalizeHeaders = void 0;
4
4
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
5
5
  const types_1 = require("unnbound-logger-sdk/dist/types");
6
6
  const trace_1 = require("unnbound-logger-sdk/dist/trace");
@@ -55,8 +55,7 @@ const parseBody = async (request, headers) => {
55
55
  return await request.parseBody();
56
56
  if (headers['content-type']?.includes('text/plain'))
57
57
  return await request.text();
58
- // TODO: handle binary body
59
- return undefined;
58
+ return request.arrayBuffer();
60
59
  }
61
60
  catch {
62
61
  return undefined;
@@ -114,3 +113,7 @@ const buildIncomingPayload = (event, result) => {
114
113
  };
115
114
  };
116
115
  exports.buildIncomingPayload = buildIncomingPayload;
116
+ const batchArray = (array, size) => {
117
+ return [...Array(Math.ceil(array.length / size))].map((_, i) => array.slice(i * size, i * size + size));
118
+ };
119
+ exports.batchArray = batchArray;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unnbound-events",
3
3
  "description": "Unified events SDK to handle HTTP routes and queued messages with a single routing API.",
4
- "version": "2.0.13",
4
+ "version": "2.0.15",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "Unnbound Team",
@@ -20,7 +20,7 @@
20
20
  "@hono/node-server": "^1.19.6",
21
21
  "axios": "^1.12.2",
22
22
  "hono": "^4.10.4",
23
- "unnbound-logger-sdk": "3.0.25"
23
+ "unnbound-logger-sdk": "3.0.26"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/jest": "^29.5.12",