unnbound-events 2.0.9 → 2.0.11

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { IncomingEvent, IncomingRequest, EventMetadata, EventSource } from './lib/types';
1
+ export type { IncomingEvent, IncomingRequest, EventMetadata, EventSource } from './lib/event';
2
2
  export type { EventServerOptions, EventServer } from './lib/server';
3
3
  export { createServer } from './lib/server';
4
4
  export { createEnqueuer } from './lib/enqueue';
@@ -15,6 +15,6 @@ export declare class ExtendedSQSClient extends SQSClient {
15
15
  constructor(config: ExtendedSQSClientConfig);
16
16
  private isS3PointerMessage;
17
17
  private retrieveMessage;
18
- receive(command: ReceiveMessageCommand): Promise<ReceiveMessageCommandOutput>;
18
+ receive(command: ReceiveMessageCommand, options?: Parameters<SQSClient['send']>[1]): Promise<ReceiveMessageCommandOutput>;
19
19
  }
20
20
  export {};
@@ -24,15 +24,17 @@ class ExtendedSQSClient extends client_sqs_1.SQSClient {
24
24
  if (!this.isS3PointerMessage(parsed))
25
25
  return message;
26
26
  const { bucket, key } = parsed[1];
27
- unnbound_logger_sdk_1.logger.debug({ bucket, key, ...(0, internal_1.internal)() }, 'Retrieving message from storage.');
28
- const payload = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }));
29
- const body = await payload.Body?.transformToString();
30
- if (!body)
31
- throw new Error('Failed to retrieve queue message from storage.');
27
+ const body = await (0, unnbound_logger_sdk_1.startSpan)('Retrieve message from storage', async () => {
28
+ const payload = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }));
29
+ const body = await payload.Body?.transformToString();
30
+ if (!body)
31
+ 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 }));
32
34
  return { ...message, Body: body };
33
35
  }
34
- async receive(command) {
35
- const result = await this.send(command);
36
+ async receive(command, options) {
37
+ const result = await this.send(command, options);
36
38
  return {
37
39
  ...result,
38
40
  Messages: await Promise.all(result.Messages?.map(this.retrieveMessage.bind(this)) ?? []),
@@ -1,14 +1,14 @@
1
- import { UnnboundLogger } from 'unnbound-logger-sdk';
1
+ import { ILogger } from 'unnbound-logger-sdk';
2
2
  import { Hono } from 'hono';
3
3
  export interface HttpAdapterOptions {
4
4
  app: Hono<any, any, any>;
5
- logger?: UnnboundLogger;
5
+ logger: ILogger;
6
6
  ignoreTraceRoutes?: string[];
7
7
  }
8
8
  export declare class HttpAdapter {
9
9
  private readonly logger;
10
10
  private readonly app;
11
11
  constructor({ app, ...options }: HttpAdapterOptions);
12
- listen(): () => Promise<void>;
12
+ listen(): Promise<() => Promise<void>>;
13
13
  private close;
14
14
  }
@@ -11,25 +11,34 @@ class HttpAdapter {
11
11
  this.logger = options.logger ?? unnbound_logger_sdk_1.logger;
12
12
  this.app = app;
13
13
  }
14
- listen() {
14
+ async listen() {
15
15
  try {
16
16
  const port = parseInt(process.env.PORT ?? '3000');
17
- const server = (0, node_server_1.serve)({ fetch: this.app.fetch, port });
18
- this.logger?.info((0, internal_1.internal)(), 'HTTP server started.');
17
+ const server = await new Promise((resolve, reject) => {
18
+ try {
19
+ const server = (0, node_server_1.serve)({ fetch: this.app.fetch, port }, () => resolve(server));
20
+ }
21
+ catch (error) {
22
+ reject(error);
23
+ }
24
+ });
25
+ this.logger.debug({ port, ...(0, internal_1.internal)() }, 'HTTP server started.');
19
26
  return () => this.close(server);
20
27
  }
21
28
  catch (error) {
22
- this.logger?.info({ err: error, ...(0, internal_1.internal)() }, 'HTTP server failed to start.');
29
+ this.logger.error({ err: error, ...(0, internal_1.internal)() }, 'HTTP server failed to start.');
23
30
  throw error;
24
31
  }
25
32
  }
26
33
  close(server) {
27
34
  return new Promise((resolve, reject) => {
28
- this.logger?.info((0, internal_1.internal)(), 'Http sever stopping...');
35
+ this.logger.debug((0, internal_1.internal)(), 'Http server stopping...');
29
36
  server.close((error) => {
30
- if (error)
37
+ if (error) {
38
+ this.logger.error({ err: error }, 'Http server failed to stop.');
31
39
  return reject(error);
32
- this.logger?.info((0, internal_1.internal)(), 'Http sever stopped.');
40
+ }
41
+ this.logger.debug((0, internal_1.internal)(), 'Http server stopped.');
33
42
  return resolve();
34
43
  });
35
44
  });
@@ -1,6 +1,5 @@
1
- import { UnnboundLogger } from 'unnbound-logger-sdk';
2
- import { Message } from '@aws-sdk/client-sqs';
3
- import { EventHandlerResult, IncomingEvent, IncomingRequest } from '../types';
1
+ import { ILogger } from 'unnbound-logger-sdk';
2
+ import { Hono } from 'hono';
4
3
  export interface SqsRequest {
5
4
  method: string;
6
5
  url: string;
@@ -15,23 +14,26 @@ export interface SqsMessage {
15
14
  request: SqsRequest;
16
15
  metadata: SqsMetadata;
17
16
  }
18
- export type QueueEventHandler = (request: IncomingEvent<IncomingRequest>) => Promise<EventHandlerResult>;
17
+ export interface QueueOptions {
18
+ readonly maxMessages?: number;
19
+ readonly visibilityTimeout?: number;
20
+ }
19
21
  export interface QueueAdapterOptions {
20
- logger?: UnnboundLogger;
21
- maxMessages?: number;
22
- visibilityTimeout?: number | undefined;
23
- handler: QueueEventHandler;
22
+ logger: ILogger;
23
+ app: Hono<any, any, any>;
24
+ options?: QueueOptions;
24
25
  }
25
26
  export declare class QueueAdapter {
26
27
  private readonly logger;
28
+ private readonly app;
27
29
  private readonly sqs;
28
30
  private readonly options;
29
- private readonly queue?;
30
- private readonly handler;
31
- constructor({ maxMessages, visibilityTimeout, handler, ...options }: QueueAdapterOptions);
32
- parseEvent(message: Message): IncomingEvent<IncomingRequest>;
33
- listen(): () => Promise<void>;
34
- private getReceiveOptions;
31
+ constructor({ logger, app, options }: QueueAdapterOptions);
32
+ private getQueue;
33
+ listen(): Promise<() => Promise<void>>;
34
+ private processMessage;
35
+ private getReceiveCommand;
35
36
  private delete;
36
37
  private isSqsMessage;
38
+ private parseEvent;
37
39
  }
@@ -9,113 +9,117 @@ const utils_1 = require("../utils");
9
9
  const extended_sqs_client_1 = require("./extended-sqs-client");
10
10
  class QueueAdapter {
11
11
  logger;
12
+ app;
12
13
  sqs;
13
14
  options;
14
- queue;
15
- handler;
16
- constructor({ maxMessages, visibilityTimeout, handler, ...options }) {
17
- this.logger = options.logger ?? unnbound_logger_sdk_1.logger;
18
- this.options = { maxMessages, visibilityTimeout };
19
- this.handler = handler;
15
+ constructor({ logger, app, options }) {
16
+ this.logger = logger;
17
+ this.app = app;
18
+ this.options = {
19
+ ...options,
20
+ maxMessages: options?.maxMessages ?? 10,
21
+ visibilityTimeout: options?.visibilityTimeout ?? 20,
22
+ };
20
23
  const region = process.env.UNNBOUND_AWS_REGION ?? process.env.AWS_REGION ?? 'us-west-1';
21
- const s3 = new client_s3_1.S3Client({ region, endpoint: process.env.UNNBOUND_S3_ENDPOINT });
22
- this.sqs = new extended_sqs_client_1.ExtendedSQSClient({
23
- region,
24
- endpoint: process.env.UNNBOUND_SQS_ENDPOINT,
25
- s3,
26
- });
27
- this.queue = process.env.UNNBOUND_SQS_URL;
24
+ const endpoint = process.env.UNNBOUND_S3_ENDPOINT;
25
+ const s3 = new client_s3_1.S3Client({ region, endpoint, maxAttempts: 3 });
26
+ this.sqs = new extended_sqs_client_1.ExtendedSQSClient({ region, endpoint, s3, maxAttempts: 3 });
28
27
  }
29
- parseEvent(message) {
30
- try {
31
- if (!message.Body)
32
- throw new Error('Queue message body is missing.');
33
- const parsed = JSON.parse(message.Body);
34
- if (!this.isSqsMessage(parsed))
35
- throw new Error('Invalid queue message format.');
36
- const method = parsed.request.method.toLowerCase();
37
- const url = new URL(parsed.request.url, `https://unnbound.ai`);
38
- const request = {
39
- url: url.toString(),
40
- method,
41
- path: url.pathname,
42
- headers: (0, utils_1.normalizeHeaders)(parsed.request.headers),
43
- query: Object.fromEntries(url.searchParams.entries()),
44
- body: parsed.request.body
45
- ? Buffer.from(parsed.request.body, 'base64').toString('utf-8')
46
- : undefined,
47
- };
48
- return {
49
- request,
50
- timestamp: parsed.timestamp,
51
- metadata: {
52
- ...parsed.metadata,
53
- ...(0, utils_1.getMetadataFromRequest)(request, 'queue'),
54
- },
55
- };
56
- }
57
- catch (error) {
58
- throw new Error('Failed to parse queue message.', { cause: error });
59
- }
28
+ getQueue() {
29
+ const queue = process.env.UNNBOUND_SQS_URL;
30
+ if (queue)
31
+ return queue;
32
+ throw new Error('UNNBOUND_SQS_URL is not configured.');
60
33
  }
61
- listen() {
62
- if (!this.queue) {
63
- this.logger?.warn({}, 'Queue adapter configuration is not provided. Reach out to support.');
64
- return () => Promise.resolve();
65
- }
66
- let stopped = false;
67
- const loop = async () => {
68
- this.logger?.debug({ queue: this.queue, ...(0, internal_1.internal)() }, 'Queue listener starting...');
69
- const command = new client_sqs_1.ReceiveMessageCommand({
70
- QueueUrl: this.queue,
71
- ...this.getReceiveOptions(false),
72
- });
73
- await this.sqs.receive(command);
74
- while (!stopped) {
34
+ async listen() {
35
+ this.logger.debug((0, internal_1.internal)(), 'Queue listener starting...');
36
+ await this.sqs.receive(this.getReceiveCommand(true));
37
+ this.logger.debug((0, internal_1.internal)(), 'Queue listener started.');
38
+ const controller = new AbortController();
39
+ const loop = new Promise((resolve, reject) => {
40
+ const loop = async () => {
41
+ if (controller.signal.aborted)
42
+ return resolve(void 0);
75
43
  try {
76
- const command = new client_sqs_1.ReceiveMessageCommand({
77
- QueueUrl: this.queue,
78
- ...this.getReceiveOptions(false),
44
+ const { Messages: messages } = await this.sqs.receive(this.getReceiveCommand(false), {
45
+ abortSignal: controller.signal,
79
46
  });
80
- const { Messages: messages } = await this.sqs.receive(command);
81
47
  if (!messages?.length)
82
- continue;
83
- const results = await Promise.all(messages.map(async (message) => await this.handler(this.parseEvent(message))
84
- .then((result) => {
85
- if (!result?.status || result.status < 400)
86
- return undefined;
87
- return message;
88
- })
89
- .catch(() => undefined)));
90
- await this.delete(results.filter((result) => !!result));
48
+ return;
49
+ const { failed } = await (0, unnbound_logger_sdk_1.startSpan)('Process messages', async () => {
50
+ const results = await Promise.all(messages.map(this.processMessage.bind(this)));
51
+ const failed = results.filter((result) => !!result);
52
+ return { failed, succeeded: results.length - failed.length, total: results.length };
53
+ }, (o) => ({ ...(0, internal_1.internal)(), ...o?.result, failed: o?.result?.failed?.length }));
54
+ await this.delete(failed);
91
55
  }
92
56
  catch (err) {
93
- this.logger?.error({ err }, 'Queue loop error.');
57
+ if (err instanceof Error && err.name === 'AbortError')
58
+ return;
59
+ this.logger.error({ err }, 'Queue loop error.');
94
60
  }
95
- }
96
- };
97
- const promise = loop();
61
+ finally {
62
+ process.nextTick(loop);
63
+ }
64
+ };
65
+ loop().catch(reject);
66
+ });
98
67
  return async () => {
99
- stopped = true;
100
- this.logger?.info((0, internal_1.internal)(), 'Queue listener stopping...');
101
- await promise.catch(() => void 0);
102
- this.logger?.info((0, internal_1.internal)(), 'Queue listener stopped.');
68
+ controller.abort();
69
+ this.logger.debug((0, internal_1.internal)(), 'Queue listener stopping...');
70
+ try {
71
+ await loop;
72
+ this.logger.debug((0, internal_1.internal)(), 'Queue listener stopped.');
73
+ }
74
+ catch (error) {
75
+ this.logger.error({ err: error }, 'Queue listener failed to stop.');
76
+ throw error;
77
+ }
103
78
  };
104
79
  }
105
- getReceiveOptions(peek) {
106
- if (peek)
107
- return { MaxNumberOfMessages: 1, WaitTimeSeconds: 0, VisibilityTimeout: 0 };
108
- return {
109
- MaxNumberOfMessages: this.options.maxMessages ?? 10,
110
- WaitTimeSeconds: 20,
111
- VisibilityTimeout: this.options.visibilityTimeout,
112
- };
80
+ async processMessage(message) {
81
+ try {
82
+ const event = this.parseEvent(message);
83
+ // It forward the request to the HTTP server that will
84
+ // handle it according to the routes the user set up.
85
+ const response = await this.app.request(event.request.url, {
86
+ method: event.request.method,
87
+ body: event.request.body,
88
+ headers: {
89
+ ...event.request.headers,
90
+ ...(0, utils_1.serializeMetadata)({ source: 'queue', timestamp: event.timestamp }),
91
+ },
92
+ });
93
+ if (response.status >= 400)
94
+ return;
95
+ return message;
96
+ }
97
+ catch (error) {
98
+ this.logger.error({ err: error }, 'Failed to handle queue event.');
99
+ }
100
+ }
101
+ getReceiveCommand(peek) {
102
+ const maxMessages = this.options.maxMessages ?? 10;
103
+ if (maxMessages < 1)
104
+ throw new Error('Max messages must be greater than 0.');
105
+ if (maxMessages > 10)
106
+ throw new Error('Max messages must be less than or equal to 10.');
107
+ return new client_sqs_1.ReceiveMessageCommand({
108
+ QueueUrl: this.getQueue(),
109
+ ...(peek
110
+ ? { MaxNumberOfMessages: 1, WaitTimeSeconds: 0, VisibilityTimeout: 0 }
111
+ : {
112
+ MaxNumberOfMessages: maxMessages,
113
+ WaitTimeSeconds: 20,
114
+ VisibilityTimeout: this.options.visibilityTimeout ?? 0,
115
+ }),
116
+ });
113
117
  }
114
118
  async delete(messages) {
115
119
  if (!messages.length)
116
120
  return;
117
121
  await this.sqs.send(new client_sqs_1.DeleteMessageBatchCommand({
118
- QueueUrl: this.queue,
122
+ QueueUrl: this.getQueue(),
119
123
  Entries: messages.map((m) => ({ Id: m.MessageId, ReceiptHandle: m.ReceiptHandle })),
120
124
  }));
121
125
  }
@@ -126,5 +130,32 @@ class QueueAdapter {
126
130
  'timestamp' in message &&
127
131
  'request' in message);
128
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
+ }
129
160
  }
130
161
  exports.QueueAdapter = QueueAdapter;
@@ -1,2 +1,6 @@
1
- import { EventMetadata, IncomingRequest } from './types';
2
- export declare const createEnqueuer: () => (event: IncomingRequest, metadata?: EventMetadata) => Promise<undefined>;
1
+ import { EventMetadata, IncomingRequest } from './event';
2
+ export interface EnqueuedRequest extends Omit<IncomingRequest, 'url' | 'headers' | 'query'> {
3
+ query?: IncomingRequest['query'];
4
+ headers?: IncomingRequest['headers'];
5
+ }
6
+ export declare const createEnqueuer: () => (event: EnqueuedRequest, metadata?: Partial<EventMetadata>) => Promise<undefined>;
@@ -9,7 +9,14 @@ const types_1 = require("unnbound-logger-sdk/dist/types");
9
9
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
10
10
  const internal_1 = require("unnbound-logger-sdk/dist/internal");
11
11
  const storage_1 = require("unnbound-logger-sdk/dist/storage");
12
- const getWorkspaceId = (url) => url.replace(/^https?:\/\//, '').replace(/\.unnbound\.ai$/, '');
12
+ const getWorkspaceId = (url) => {
13
+ url = url.replace(/^https?:\/\//, '');
14
+ if (!url.includes('unnbound.ai'))
15
+ return url.replace(/^.+?\/(.+)$/, '$1').replace(/^a?sync\//, '');
16
+ if (url.startsWith('workflow.'))
17
+ return url.replace(/^.+?\/(.+)$/, '$1').replace(/^a?sync\//, '');
18
+ return url.replace(/\.unnbound\.ai$/, '').replace(/^a?sync\//, '');
19
+ };
13
20
  const getEnvironment = (url) => (url.includes('stg.unnbound.ai') ? 'stg' : 'prod');
14
21
  const getWorkflowDomain = (url) => {
15
22
  return getEnvironment(url) === 'stg' ? 'workflow.stg.unnbound.ai' : 'workflow.unnbound.ai';
@@ -20,6 +27,7 @@ const createEnqueuer = () => {
20
27
  throw new Error('UNNBOUND_WORKFLOW_URL is not configured. Reach out to support.');
21
28
  const client = (0, unnbound_logger_sdk_1.traceAxios)(axios_1.default.create({ baseURL: `https://${getWorkflowDomain(url)}/async/${getWorkspaceId(url)}` }), { getPayload: internal_1.internal });
22
29
  return async (event, metadata) => {
30
+ unnbound_logger_sdk_1.logger.info({ event, metadata }, 'Enqueuing event...');
23
31
  if (!event.path.startsWith('/'))
24
32
  throw new Error('Path must be relative.');
25
33
  const { traceId, messageId } = storage_1.storage.getStore() ?? {};
@@ -1,5 +1,5 @@
1
1
  import { StatusCode } from 'hono/utils/http-status';
2
- import { StrictEventHandlerResult } from './types';
2
+ import { StrictEventHandlerResult } from './event';
3
3
  interface HandlerErrorOptions extends ErrorOptions {
4
4
  status?: StatusCode;
5
5
  message?: string;
package/dist/lib/error.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HandlerError = void 0;
4
- const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
5
4
  class HandlerError extends Error {
6
5
  status;
7
6
  message;
@@ -17,11 +16,13 @@ class HandlerError extends Error {
17
16
  return new HandlerError({ message: 'Internal server error', status: 500, cause: error });
18
17
  }
19
18
  toError() {
20
- return this.cause && this.cause instanceof Error ? this.cause : this;
19
+ if (this.cause instanceof HandlerError)
20
+ return this.cause.toError();
21
+ if (this.cause instanceof Error)
22
+ return this.cause;
23
+ return this;
21
24
  }
22
25
  toJson() {
23
- const err = this.toError();
24
- unnbound_logger_sdk_1.logger.error({ err }, err.message);
25
26
  return { status: this.status, body: { message: this.message } };
26
27
  }
27
28
  }
@@ -0,0 +1,30 @@
1
+ import type { HttpMethod } from 'unnbound-logger-sdk';
2
+ import { StatusCode } from 'hono/utils/http-status';
3
+ export interface IncomingRequest<B = any> {
4
+ url: string;
5
+ method: HttpMethod;
6
+ path: string;
7
+ headers: Record<string, string>;
8
+ query: Record<string, any>;
9
+ body?: B;
10
+ }
11
+ export interface MatchedIncomingRequest<B = any> extends IncomingRequest<B> {
12
+ params?: Record<string, string> | undefined;
13
+ }
14
+ export type EventSource = 'http' | 'queue';
15
+ export interface EventMetadata {
16
+ traceId: string;
17
+ messageId: string;
18
+ source: EventSource;
19
+ }
20
+ export interface IncomingEvent<R extends IncomingRequest = IncomingRequest> {
21
+ timestamp: number;
22
+ request: R;
23
+ metadata: EventMetadata;
24
+ }
25
+ export interface StrictEventHandlerResult {
26
+ status: StatusCode;
27
+ headers?: Record<string, string>;
28
+ body?: object | undefined | null;
29
+ }
30
+ export type EventHandlerResult = Partial<StrictEventHandlerResult> | undefined | null | void;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,17 +1,19 @@
1
1
  import { Context } from 'hono';
2
- import { EventHandlerResult } from '../types';
2
+ import { EventHandlerResult, StrictEventHandlerResult } from '../event';
3
3
  import { MiddlewareHandler } from './middleware';
4
- import { EventContext, MaybePromise } from './types';
4
+ import { EventEnvironment, EventVariables, MaybePromise } from './types';
5
5
  export type Handler<I, R = EventHandlerResult> = (input: I) => MaybePromise<R>;
6
6
  export interface Endpoint<I> {
7
7
  (path: string, handler: Handler<I>): MaybePromise<void>;
8
8
  (path: string, middleware: MiddlewareHandler<I>, handler: Handler<I>): MaybePromise<void>;
9
9
  }
10
10
  export type EndpointArgs<I> = [path: string, handler: Handler<I>] | [path: string, middleware: MiddlewareHandler<I>, handler: Handler<I>];
11
- export declare const buildEndpointArguments: (...args: EndpointArgs<EventContext>) => (string | MiddlewareHandler<Context<any, any, {}>> | Handler<Context<any, any, {}>, Response>)[];
12
- export declare const parseEndpointArguments: (...args: EndpointArgs<EventContext>) => {
11
+ export declare const buildEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => (string | MiddlewareHandler<Context<EventEnvironment<{}>, any, {}>> | Handler<Context<EventEnvironment<{}>, any, {}>, Response>)[];
12
+ export declare const parseEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => {
13
13
  path: string;
14
- middlewares: MiddlewareHandler<EventContext>[];
15
- handler: Handler<EventContext, EventHandlerResult>;
14
+ middlewares: MiddlewareHandler<EventVariables<{}>>[];
15
+ handler: Handler<EventVariables<{}>, EventHandlerResult>;
16
16
  };
17
- export declare const patchEndpoint: (handler: Handler<EventContext>) => Handler<Context, Response>;
17
+ export declare const patchEndpoint: <V extends object>(handler: Handler<EventVariables<V>>) => Handler<Context<EventEnvironment<V>>, Response>;
18
+ export declare const respondWith: (context: Context, result: StrictEventHandlerResult) => (Response & import("hono").TypedResponse<null, 100 | 101 | 102 | 103 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "body">) | (Response & import("hono").TypedResponse<never, 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">);
19
+ export declare const attachTraceHeaders: (headers?: Record<string, string>) => Record<string, string> | undefined;
@@ -1,13 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.patchEndpoint = exports.parseEndpointArguments = exports.buildEndpointArguments = void 0;
3
+ exports.attachTraceHeaders = exports.respondWith = exports.patchEndpoint = exports.parseEndpointArguments = exports.buildEndpointArguments = void 0;
4
+ const types_1 = require("unnbound-logger-sdk/dist/types");
5
+ const storage_1 = require("unnbound-logger-sdk/dist/storage");
4
6
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
5
7
  const utils_1 = require("../utils");
6
- const error_1 = require("../error");
7
8
  const middleware_1 = require("./middleware");
8
9
  const buildEndpointArguments = (...args) => {
9
10
  const { path, middlewares, handler } = (0, exports.parseEndpointArguments)(...args);
10
- return [path, ...middlewares.map(middleware_1.patchMiddleware), (0, exports.patchEndpoint)(handler)];
11
+ const patchedHandler = (0, exports.patchEndpoint)(handler);
12
+ return [path, ...middlewares.map(middleware_1.patchMiddleware), patchedHandler];
11
13
  };
12
14
  exports.buildEndpointArguments = buildEndpointArguments;
13
15
  const parseEndpointArguments = (...args) => {
@@ -18,26 +20,30 @@ const parseEndpointArguments = (...args) => {
18
20
  exports.parseEndpointArguments = parseEndpointArguments;
19
21
  const patchEndpoint = (handler) => {
20
22
  return async (context) => {
21
- const event = await (0, utils_1.getIncomingEvent)(context.req);
22
- context.set('event', event);
23
- const result = await (0, unnbound_logger_sdk_1.withTrace)(() => (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => {
24
- try {
25
- const result = await handler(context.get('event'));
26
- if (result instanceof Error)
27
- return error_1.HandlerError.fromError(result).toJson();
28
- return {
29
- status: result?.status ?? 204,
30
- headers: result?.headers,
31
- body: result?.body,
32
- };
33
- }
34
- catch (error) {
35
- return error_1.HandlerError.fromError(error).toJson();
36
- }
37
- }, (o) => (0, utils_1.buildIncomingPayload)(event, o)), event.metadata);
38
- if (!result?.body)
39
- return context.body(null, result?.status ?? 204, result?.headers);
40
- return context.json(result.body, (result.status ?? 200), result.headers);
23
+ const state = context.get('state');
24
+ const result = await (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => {
25
+ const result = await handler(state);
26
+ return { status: result?.status ?? 204, headers: result?.headers, body: result?.body };
27
+ }, (o) => (0, utils_1.buildIncomingPayload)(state, o));
28
+ return (0, exports.respondWith)(context, result);
41
29
  };
42
30
  };
43
31
  exports.patchEndpoint = patchEndpoint;
32
+ const respondWith = (context, result) => {
33
+ const headers = (0, exports.attachTraceHeaders)(result.headers);
34
+ if (!result.body)
35
+ return context.body(null, result.status, headers);
36
+ return context.json(result.body, result.status, headers);
37
+ };
38
+ exports.respondWith = respondWith;
39
+ const attachTraceHeaders = (headers) => {
40
+ const { traceId, messageId } = storage_1.storage.getStore() ?? {};
41
+ if (!traceId || !messageId)
42
+ return headers;
43
+ return {
44
+ ...headers,
45
+ [types_1.defaultTraceHeaderKey]: headers?.[types_1.defaultTraceHeaderKey] ?? traceId,
46
+ [types_1.defaultMessageHeaderKey]: headers?.[types_1.defaultMessageHeaderKey] ?? messageId,
47
+ };
48
+ };
49
+ exports.attachTraceHeaders = attachTraceHeaders;
@@ -1,17 +1,17 @@
1
1
  import { Context, Next } from 'hono';
2
- import { EventContext, MaybePromise } from './types';
2
+ import { EventEnvironment, EventVariables, MaybePromise } from './types';
3
3
  export type MiddlewareHandler<I> = (input: I, next: Next) => MaybePromise<void>;
4
4
  export interface Middleware<I> {
5
5
  (...middlewares: MiddlewareHandler<I>[]): MaybePromise<void>;
6
6
  (path: string, ...middlewares: MiddlewareHandler<I>[]): MaybePromise<void>;
7
7
  }
8
8
  export type MiddlewareArgs<I> = [...middlewares: MiddlewareHandler<I>[]] | [path: string, ...middlewares: MiddlewareHandler<I>[]];
9
- export declare const patchMiddleware: (handler: MiddlewareHandler<EventContext>) => MiddlewareHandler<Context>;
10
- export declare const parseMiddlewareArguments: (...args: MiddlewareArgs<EventContext>) => {
9
+ export declare const patchMiddleware: (handler: MiddlewareHandler<EventVariables<{}>>) => MiddlewareHandler<Context<EventEnvironment<{}>>>;
10
+ export declare const parseMiddlewareArguments: (...args: MiddlewareArgs<EventVariables<{}>>) => {
11
11
  path: string;
12
- middlewares: MiddlewareHandler<EventContext>[];
12
+ middlewares: MiddlewareHandler<EventVariables<{}>>[];
13
13
  } | {
14
14
  path: undefined;
15
- middlewares: MiddlewareHandler<EventContext>[];
15
+ middlewares: MiddlewareHandler<EventVariables<{}>>[];
16
16
  };
17
- export declare const buildMiddlewareArguments: (...args: MiddlewareArgs<EventContext>) => MiddlewareHandler<EventContext>[] | readonly [string, ...MiddlewareHandler<Context<any, any, {}>>[]];
17
+ export declare const buildMiddlewareArguments: (...args: MiddlewareArgs<EventVariables<{}>>) => MiddlewareHandler<EventVariables<{}>>[] | readonly [string, ...MiddlewareHandler<Context<EventEnvironment<{}>, any, {}>>[]];
@@ -2,7 +2,7 @@
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('event'), next);
5
+ return (context, next) => handler(context.get('state'), next);
6
6
  };
7
7
  exports.patchMiddleware = patchMiddleware;
8
8
  const parseMiddlewareArguments = (...args) => {
@@ -1,8 +1,16 @@
1
1
  import { Env } from 'hono';
2
- import { IncomingEvent, MatchedIncomingRequest } from '../types';
2
+ import { IncomingEvent, MatchedIncomingRequest } from '../event';
3
3
  export type MaybePromise<V> = Promise<V> | V;
4
- export interface EventEnvironment extends Env {
5
- Variables: EventContext;
4
+ interface EventInternalVariables<V extends object> {
5
+ set: <K extends keyof V>(key: K, value: V[K]) => void;
6
+ get: <K extends keyof V>(key: K) => V[K] | undefined;
6
7
  }
7
- export interface EventContext extends IncomingEvent<MatchedIncomingRequest> {
8
+ export interface EventVariables<V extends object> extends IncomingEvent<MatchedIncomingRequest>, EventInternalVariables<V> {
8
9
  }
10
+ export interface EventContext<V extends object> {
11
+ state: EventVariables<V>;
12
+ }
13
+ export interface EventEnvironment<V extends object> extends Env {
14
+ Variables: EventContext<V>;
15
+ }
16
+ export {};
@@ -1,7 +1,7 @@
1
- import { UnnboundLogger } from 'unnbound-logger-sdk';
1
+ import { ILogger } from 'unnbound-logger-sdk';
2
2
  import { Endpoint } from './routing/endpoint';
3
3
  import { Middleware } from './routing/middleware';
4
- import { EventContext } from './routing/types';
4
+ import { EventVariables } from './routing/types';
5
5
  interface HttpOptions {
6
6
  }
7
7
  interface QueueOptions {
@@ -9,20 +9,19 @@ interface QueueOptions {
9
9
  visibilityTimeout?: number;
10
10
  }
11
11
  export interface EventServerOptions {
12
- logger?: UnnboundLogger;
12
+ logger?: ILogger;
13
13
  queue?: QueueOptions;
14
14
  http?: HttpOptions;
15
15
  }
16
- export interface EventServer {
17
- get: Endpoint<EventContext>;
18
- post: Endpoint<EventContext>;
19
- put: Endpoint<EventContext>;
20
- patch: Endpoint<EventContext>;
21
- delete: Endpoint<EventContext>;
22
- use: Middleware<EventContext>;
23
- start(): () => Promise<void>;
16
+ export interface EventServer<V extends object = {}> {
17
+ get: Endpoint<EventVariables<V>>;
18
+ post: Endpoint<EventVariables<V>>;
19
+ put: Endpoint<EventVariables<V>>;
20
+ patch: Endpoint<EventVariables<V>>;
21
+ delete: Endpoint<EventVariables<V>>;
22
+ use: Middleware<EventVariables<V>>;
23
+ start(): Promise<() => Promise<void>>;
24
24
  stop(): Promise<void>;
25
- shutdown(): Promise<void>;
26
25
  }
27
- export declare const createServer: (options?: EventServerOptions) => EventServer;
26
+ export declare const createServer: <V extends object = {}>(options?: EventServerOptions) => EventServer<V>;
28
27
  export {};
@@ -10,26 +10,33 @@ const endpoint_1 = require("./routing/endpoint");
10
10
  const middleware_1 = require("./routing/middleware");
11
11
  const utils_1 = require("./utils");
12
12
  class HttpQueueServer {
13
- options;
13
+ logger;
14
14
  app = new hono_1.Hono();
15
+ http;
16
+ queue;
15
17
  dispose;
16
18
  constructor(options = {}) {
17
- this.options = options;
18
- options.logger = options.logger ?? unnbound_logger_sdk_1.logger;
19
+ this.logger = options.logger ?? unnbound_logger_sdk_1.logger;
19
20
  this.app.get('/healthcheck', (c) => c.json({ status: 'ok' }));
21
+ this.app.use(async (context, next) => {
22
+ const event = await (0, utils_1.getIncomingEvent)(context.req);
23
+ const state = { get: context.get, set: context.set, ...event };
24
+ context.set('state', state);
25
+ return await (0, unnbound_logger_sdk_1.withTrace)(next, event.metadata);
26
+ });
20
27
  this.app.notFound((0, endpoint_1.patchEndpoint)(() => {
21
- throw new error_1.HandlerError({ status: 404, message: 'Not found.' });
28
+ throw new error_1.HandlerError({ status: 404, message: 'Endpoint not found.' });
22
29
  }));
23
- this.app.onError((err, c) => c.json(new error_1.HandlerError({ status: 500, message: 'Internal server error.', cause: err }).toJson()));
24
- process.on('SIGINT', async () => {
25
- this.options.logger?.debug({}, 'Received SIGINT. Initiating graceful shutdown...');
26
- await this.shutdown();
27
- process.exit(0);
30
+ this.app.onError((err, c) => {
31
+ const error = error_1.HandlerError.fromError(err);
32
+ unnbound_logger_sdk_1.logger.error({ err: error.toError() }, error.message);
33
+ return (0, endpoint_1.respondWith)(c, error.toJson());
28
34
  });
29
- process.on('SIGTERM', async () => {
30
- this.options.logger?.debug({}, 'Received SIGTERM. Initiating graceful shutdown...');
31
- await this.shutdown();
32
- process.exit(0);
35
+ this.http = new http_adapter_1.HttpAdapter({ app: this.app, logger: this.logger, ...options.http });
36
+ this.queue = new queue_adapter_1.QueueAdapter({
37
+ app: this.app,
38
+ logger: this.logger,
39
+ options: options.queue,
33
40
  });
34
41
  }
35
42
  get = (...args) => {
@@ -56,44 +63,36 @@ class HttpQueueServer {
56
63
  // @ts-expect-error
57
64
  return this.app.use(...(0, middleware_1.buildMiddlewareArguments)(...args));
58
65
  };
59
- start() {
60
- const adapters = [
61
- new http_adapter_1.HttpAdapter({
62
- app: this.app,
63
- logger: this.options.logger,
64
- ...this.options.http,
65
- }),
66
- new queue_adapter_1.QueueAdapter({
67
- handler: async (event) => {
68
- // This will forward the request to the HTTP server
69
- // that will handle it according to the routes the user set up.
70
- await this.app.request(event.request.url, {
71
- method: event.request.method,
72
- body: event.request.body,
73
- headers: {
74
- ...event.request.headers,
75
- ...(0, utils_1.serializeMetadata)({ source: 'queue', timestamp: event.timestamp }),
76
- },
77
- });
78
- },
79
- logger: this.options.logger,
80
- ...this.options.queue,
81
- }),
82
- ];
83
- const disposables = adapters.map((adapter) => adapter.listen());
84
- this.dispose = () => Promise.all(disposables).then(() => void 0);
85
- this.options.logger?.info({}, 'Server started.');
86
- return this.dispose.bind(this);
66
+ async start() {
67
+ const createOnSignal = (signal) => async () => {
68
+ this.logger.debug({}, `Received ${signal}. Initiating graceful shutdown...`);
69
+ await this.stop();
70
+ process.exit(0);
71
+ };
72
+ process.on('SIGINT', createOnSignal('SIGINT'));
73
+ process.on('SIGTERM', createOnSignal('SIGTERM'));
74
+ this.logger.debug({}, 'Starting server...');
75
+ try {
76
+ const disposables = await Promise.all([this.http.listen(), this.queue.listen()]);
77
+ this.dispose = async () => {
78
+ await Promise.all(disposables.map((dispose) => dispose()))
79
+ .then(() => void 0)
80
+ .catch(console.error);
81
+ };
82
+ this.logger.info('Server started.');
83
+ return this.dispose;
84
+ }
85
+ catch (error) {
86
+ this.logger.error({ err: error }, 'Server failed to start.');
87
+ throw error;
88
+ }
87
89
  }
88
90
  async stop() {
89
- this.options.logger?.debug({}, 'Event client stopping...');
90
- await this.dispose?.();
91
- this.options.logger?.debug({}, 'Event client stopped.');
92
- }
93
- async shutdown() {
94
- this.options.logger?.debug({}, 'Event client shutting down...');
95
- await this.stop();
96
- this.options.logger?.debug({}, 'Event client shut down.');
91
+ if (!this.dispose)
92
+ return;
93
+ this.logger.debug({ dispose: !!this.dispose }, 'Server stopping...');
94
+ await this.dispose();
95
+ this.logger.debug({}, 'Server stopped.');
97
96
  }
98
97
  }
99
98
  const createServer = (options = {}) => new HttpQueueServer(options);
@@ -1,6 +1,6 @@
1
1
  import { Maybe } from 'unnbound-logger-sdk/dist/types';
2
2
  import { HonoRequest } from 'hono';
3
- import { EventHandlerResult, EventMetadata, EventSource, IncomingEvent, IncomingRequest, MatchedIncomingRequest, StrictEventHandlerResult } from './types';
3
+ import { EventHandlerResult, EventMetadata, EventSource, IncomingEvent, IncomingRequest, MatchedIncomingRequest, StrictEventHandlerResult } from './event';
4
4
  export declare const normalizeHeaders: (headers: Record<string, string | string[]>) => Record<string, string>;
5
5
  export declare const getIncomingEvent: (event: HonoRequest) => Promise<IncomingEvent<MatchedIncomingRequest>>;
6
6
  export declare const getMetadataFromRequest: (request: IncomingRequest, source: EventSource) => EventMetadata;
@@ -15,8 +15,9 @@ export declare const serializeMetadata: (metadata: ForwardedMetadata) => {
15
15
  export declare const buildIncomingPayload: (event: IncomingEvent<MatchedIncomingRequest>, result?: Maybe<EventHandlerResult>) => {
16
16
  type: string;
17
17
  source: EventSource;
18
- delay: number;
18
+ delay: number | undefined;
19
19
  http: {
20
+ response?: StrictEventHandlerResult | undefined;
20
21
  url: string;
21
22
  method: string;
22
23
  query: Record<string, any>;
@@ -27,7 +28,6 @@ export declare const buildIncomingPayload: (event: IncomingEvent<MatchedIncoming
27
28
  headers: Record<string, string>;
28
29
  body: any;
29
30
  };
30
- response: StrictEventHandlerResult;
31
31
  };
32
32
  };
33
33
  export {};
package/dist/lib/utils.js CHANGED
@@ -34,19 +34,29 @@ const parseParams = (request) => {
34
34
  };
35
35
  const getIncomingRequest = async (request) => {
36
36
  const url = new URL(request.url, `https://unnbound.ai`);
37
+ const headers = (0, exports.normalizeHeaders)(request.header());
37
38
  return {
38
39
  url: url.toString(),
39
40
  method: request.method.toLowerCase(),
40
41
  path: request.path,
41
- headers: (0, exports.normalizeHeaders)(request.header()),
42
+ headers,
42
43
  query: request.query(),
43
- body: await parseBody(request),
44
+ body: await parseBody(request, headers),
44
45
  params: parseParams(request),
45
46
  };
46
47
  };
47
- const parseBody = async (request) => {
48
+ const parseBody = async (request, headers) => {
48
49
  try {
49
- return await request.json();
50
+ if (headers['content-type']?.includes('application/json'))
51
+ return await request.json();
52
+ if (headers['content-type']?.includes('application/x-www-form-urlencoded'))
53
+ return await request.parseBody();
54
+ if (headers['content-type']?.includes('multipart/form-data'))
55
+ return await request.parseBody();
56
+ if (headers['content-type']?.includes('text/plain'))
57
+ return await request.text();
58
+ // TODO: handle binary body
59
+ return undefined;
50
60
  }
51
61
  catch {
52
62
  return undefined;
@@ -69,21 +79,19 @@ const deserializeMetadata = (request) => ({
69
79
  source: request.headers[sourceKey],
70
80
  timestamp: request.headers[timestampKey] ? parseInt(request.headers[timestampKey]) : undefined,
71
81
  });
72
- const buildResponsePayload = (result) => {
73
- return {
74
- status: result?.status ?? 204,
75
- headers: {
76
- 'content-type': 'application/json; charset=utf-8',
77
- ...(result?.headers ? (0, exports.normalizeHeaders)(result.headers) : undefined),
78
- },
79
- body: result?.body ? (0, utils_1.safeJsonParse)(result.body) : undefined,
80
- };
81
- };
82
+ const buildResponsePayload = (result) => ({
83
+ status: result?.status ?? 204,
84
+ headers: {
85
+ 'content-type': 'application/json; charset=utf-8',
86
+ ...(result?.headers ? (0, exports.normalizeHeaders)(result.headers) : undefined),
87
+ },
88
+ body: result?.body ? (0, utils_1.safeJsonParse)(result.body) : undefined,
89
+ });
82
90
  const buildIncomingPayload = (event, result) => {
83
91
  return {
84
92
  type: 'http',
85
93
  source: event.metadata.source,
86
- delay: Date.now() - event.timestamp,
94
+ delay: event.metadata.source === 'queue' ? Date.now() - event.timestamp : undefined,
87
95
  http: {
88
96
  url: event.request.url,
89
97
  method: event.request.method.toLowerCase(),
@@ -97,9 +105,11 @@ const buildIncomingPayload = (event, result) => {
97
105
  headers: event.request.headers,
98
106
  body: (0, utils_1.safeJsonParse)(event.request.body),
99
107
  },
100
- response: result?.error
101
- ? buildResponsePayload(error_1.HandlerError.fromError(result.error).toJson())
102
- : buildResponsePayload(result?.result),
108
+ ...(result && {
109
+ response: result.error
110
+ ? buildResponsePayload(error_1.HandlerError.fromError(result.error).toJson())
111
+ : buildResponsePayload(result.result),
112
+ }),
103
113
  },
104
114
  };
105
115
  };
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.9",
4
+ "version": "2.0.11",
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.24"
23
+ "unnbound-logger-sdk": "3.0.25"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/jest": "^29.5.12",