@pikku/lambda 0.7.0 → 0.9.0

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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.8.0
2
+
3
+ ## 0.9.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - Normalized all transports to use "wirings" instead of events/routes/transports for consistency across the framework
8
+
9
+
10
+ ### Major Features
11
+
12
+ - **SQS Queue Support**: Added SQS queue worker support
13
+
1
14
  ## 0.7.0
2
15
 
3
16
  - Updating to match remaining packages
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './http/index.js';
2
2
  export * from './websocket/index.js';
3
+ export * from './queue/index.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './http/index.js';
2
2
  export * from './websocket/index.js';
3
+ export * from './queue/index.js';
3
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA"}
@@ -0,0 +1 @@
1
+ export { runSQSQueueWorker } from './sqs-worker.js';
@@ -0,0 +1,2 @@
1
+ export { runSQSQueueWorker } from './sqs-worker.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/queue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { SQSBatchResponse, SQSEvent, SQSRecord } from 'aws-lambda';
2
+ import type { CoreSingletonServices, CreateSessionServices, QueueJob } from '@pikku/core';
3
+ export declare function mapSQSRecordToQueueJob(record: SQSRecord, maxAttempts?: number): QueueJob;
4
+ export declare const runSQSQueueWorker: (singletonServices: CoreSingletonServices, createSessionServices: CreateSessionServices<any, any, any> | undefined, event: SQSEvent) => Promise<SQSBatchResponse>;
@@ -0,0 +1,59 @@
1
+ import { runQueueJob } from '@pikku/core/queue';
2
+ export function mapSQSRecordToQueueJob(record, maxAttempts) {
3
+ let data;
4
+ try {
5
+ data = JSON.parse(record.body);
6
+ }
7
+ catch {
8
+ data = record.body;
9
+ }
10
+ if (!record.eventSourceARN) {
11
+ throw new Error('SQS record does not have eventSourceARN');
12
+ }
13
+ const queueName = record.eventSourceARN.split(':').pop();
14
+ const job = {
15
+ queueName,
16
+ data,
17
+ id: record.messageId,
18
+ status: async () => 'active',
19
+ metadata: () => ({
20
+ processedAt: new Date(),
21
+ attemptsMade: parseInt(record.attributes.ApproximateReceiveCount) - 1,
22
+ maxAttempts,
23
+ result: undefined,
24
+ progress: 0,
25
+ createdAt: new Date(parseInt(record.attributes.SentTimestamp)),
26
+ completedAt: undefined,
27
+ failedAt: undefined,
28
+ error: undefined,
29
+ }),
30
+ waitForCompletion: async () => {
31
+ throw new Error('SQS does not support waitForCompletion - jobs are fire-and-forget');
32
+ },
33
+ };
34
+ return job;
35
+ }
36
+ export const runSQSQueueWorker = async (singletonServices, createSessionServices, event) => {
37
+ console.log(JSON.stringify(event, null, 2));
38
+ const jobs = event.Records.map(mapSQSRecordToQueueJob);
39
+ const results = await Promise.allSettled(jobs.map((job) => runQueueJob({
40
+ singletonServices,
41
+ createSessionServices,
42
+ job,
43
+ })));
44
+ results.forEach((result, index) => {
45
+ if (result.status === 'rejected') {
46
+ singletonServices.logger.error(`Failed to process SQS message ${jobs[index].id}:`, result.reason);
47
+ }
48
+ });
49
+ return {
50
+ batchItemFailures: results
51
+ .map((result, index) => ({
52
+ result,
53
+ itemIdentifier: jobs[index].id || `message-${index}`,
54
+ }))
55
+ .filter(({ result }) => result.status === 'rejected')
56
+ .map(({ itemIdentifier }) => ({ itemIdentifier })),
57
+ };
58
+ };
59
+ //# sourceMappingURL=sqs-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqs-worker.js","sourceRoot":"","sources":["../../src/queue/sqs-worker.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,14 @@
1
1
  import { ChannelStore, EventHubService, EventHubStore } from '@pikku/core/channel';
2
2
  import { Logger } from '@pikku/core/services';
3
3
  import { APIGatewayEvent } from 'aws-lambda';
4
- export declare class LambdaEventHubService<Out = unknown> implements EventHubService<Out> {
4
+ export declare class LambdaEventHubService<EventTopics extends Record<string, any> = {}> implements EventHubService<EventTopics> {
5
5
  private logger;
6
6
  private channelStore;
7
7
  private eventHubStore;
8
8
  private callbackAPI;
9
- constructor(logger: Logger, event: APIGatewayEvent, channelStore: ChannelStore, eventHubStore: EventHubStore);
10
- subscribe(topic: string, channelId: string): Promise<void>;
11
- unsubscribe(topic: string, channelId: string): Promise<void>;
12
- publish(topic: string, fromChannelId: string, data: Out, isBinary?: boolean): Promise<void>;
9
+ constructor(logger: Logger, event: APIGatewayEvent, channelStore: ChannelStore, eventHubStore: EventHubStore<EventTopics>);
10
+ subscribe<T extends keyof EventTopics>(topic: T, channelId: string): Promise<void>;
11
+ unsubscribe<T extends keyof EventTopics>(topic: T, channelId: string): Promise<void>;
12
+ publish<T extends keyof EventTopics>(topic: T, channelId: string | null, data: EventTopics[T], isBinary?: boolean): Promise<void>;
13
13
  private sendMessages;
14
14
  }
@@ -16,9 +16,11 @@ export class LambdaEventHubService {
16
16
  async unsubscribe(topic, channelId) {
17
17
  await this.eventHubStore.unsubscribe(topic, channelId);
18
18
  }
19
- async publish(topic, fromChannelId, data, isBinary) {
19
+ async publish(topic, channelId, data, isBinary) {
20
20
  const channelIds = await this.eventHubStore.getChannelIdsForTopic(topic);
21
- await this.sendMessages(channelIds, fromChannelId, data);
21
+ if (channelId) {
22
+ await this.sendMessages(channelIds, channelId, data, isBinary);
23
+ }
22
24
  }
23
25
  async sendMessages(channelIds, fromChannelId, data, isBinary) {
24
26
  await sendMessages(this.logger, this.channelStore, this.callbackAPI, fromChannelId, channelIds, data, isBinary);
@@ -1 +1 @@
1
- {"version":3,"file":"lambda-eventhub-service.js","sourceRoot":"","sources":["../../src/websocket/lambda-eventhub-service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gCAAgC,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAI3E,MAAM,OAAO,qBAAqB;IAMtB;IAEA;IACA;IANF,WAAW,CAA+B;IAElD,YACU,MAAc,EACtB,KAAsB,EACd,YAA0B,EAC1B,aAA4B;QAH5B,WAAM,GAAN,MAAM,CAAQ;QAEd,iBAAY,GAAZ,YAAY,CAAc;QAC1B,kBAAa,GAAb,aAAa,CAAe;QAEpC,IAAI,CAAC,WAAW,GAAG,gCAAgC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IACpE,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,SAAiB;QAC9C,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACtD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,SAAiB;QAChD,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACxD,CAAC;IAED,KAAK,CAAC,OAAO,CACX,KAAa,EACb,aAAqB,EACrB,IAAS,EACT,QAAkB;QAElB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAA;QACxE,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,aAAa,EAAE,IAAI,CAAC,CAAA;IAC1D,CAAC;IAEO,KAAK,CAAC,YAAY,CACxB,UAAoB,EACpB,aAAqB,EACrB,IAAS,EACT,QAAkB;QAElB,MAAM,YAAY,CAChB,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,WAAW,EAChB,aAAa,EACb,UAAU,EACV,IAAI,EACJ,QAAQ,CACT,CAAA;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"lambda-eventhub-service.js","sourceRoot":"","sources":["../../src/websocket/lambda-eventhub-service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gCAAgC,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAI3E,MAAM,OAAO,qBAAqB;IAMtB;IAEA;IACA;IANF,WAAW,CAA+B;IAElD,YACU,MAAc,EACtB,KAAsB,EACd,YAA0B,EAC1B,aAAyC;QAHzC,WAAM,GAAN,MAAM,CAAQ;QAEd,iBAAY,GAAZ,YAAY,CAAc;QAC1B,kBAAa,GAAb,aAAa,CAA4B;QAEjD,IAAI,CAAC,WAAW,GAAG,gCAAgC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IACpE,CAAC;IAED,KAAK,CAAC,SAAS,CACb,KAAQ,EACR,SAAiB;QAEjB,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACtD,CAAC;IAED,KAAK,CAAC,WAAW,CACf,KAAQ,EACR,SAAiB;QAEjB,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACxD,CAAC;IAED,KAAK,CAAC,OAAO,CACX,KAAQ,EACR,SAAwB,EACxB,IAAoB,EACpB,QAAkB;QAElB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAC/D,KAAe,CAChB,CAAA;QACD,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;QAChE,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,YAAY,CACxB,UAAoB,EACpB,aAAqB,EACrB,IAAoC,EACpC,QAAkB;QAElB,MAAM,YAAY,CAChB,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,WAAW,EAChB,aAAa,EACb,UAAU,EACV,IAAI,EACJ,QAAQ,CACT,CAAA;IACH,CAAC;CACF"}
@@ -9,5 +9,5 @@ export declare const getServerlessDependencies: (logger: Logger, channelStore: C
9
9
  channelId: string;
10
10
  callbackAPI: ApiGatewayManagementApiClient;
11
11
  channelHandlerFactory: import("@pikku/core/channel").PikkuChannelHandlerFactory;
12
- channelStore: ChannelStore<unknown, unknown, import("@pikku/core/dist/index.js").CoreUserSession, import("@pikku/core/channel").Channel<unknown, unknown>>;
12
+ channelStore: ChannelStore<unknown, unknown, import("@pikku/core/src/index.js").CoreUserSession, import("@pikku/core/channel").Channel<unknown, unknown>>;
13
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/lambda",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "module": "dist/index.js",
@@ -19,11 +19,12 @@
19
19
  "exports": {
20
20
  ".": "./dist/index.js",
21
21
  "./http": "./dist/http/index.js",
22
- "./websocket": "./dist/websocket/index.js"
22
+ "./websocket": "./dist/websocket/index.js",
23
+ "./queue": "./dist/queue/index.js"
23
24
  },
24
25
  "peerDependencies": {
25
26
  "@aws-sdk/client-apigatewaymanagementapi": "^3.713.0",
26
- "@pikku/core": "^0.7.0",
27
+ "@pikku/core": "^0.9.0",
27
28
  "@types/aws-lambda": "^8.10.146"
28
29
  },
29
30
  "devDependencies": {
@@ -0,0 +1 @@
1
+ export * from './run-fetch.js'
@@ -0,0 +1,51 @@
1
+ import {
2
+ CoreSingletonServices,
3
+ CoreServices,
4
+ CreateSessionServices,
5
+ CoreUserSession,
6
+ } from '@pikku/core'
7
+ import { PikkuFetchHTTPResponse, fetchData } from '@pikku/core/http'
8
+ import { APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda'
9
+ import { responseToLambdaResult } from '../response-converter.js'
10
+ import { lambdaEventToRequest } from '../request-converter.js'
11
+
12
+ export const runFetch = async <
13
+ SingletonServices extends CoreSingletonServices,
14
+ Services extends CoreServices<SingletonServices>,
15
+ UserSession extends CoreUserSession,
16
+ >(
17
+ singletonServices: SingletonServices,
18
+ createSessionServices: CreateSessionServices<
19
+ SingletonServices,
20
+ Services,
21
+ UserSession
22
+ >,
23
+ event: APIGatewayEvent
24
+ ): Promise<APIGatewayProxyResult> => {
25
+ const request = lambdaEventToRequest(event)
26
+ const response = new PikkuFetchHTTPResponse()
27
+
28
+ if (request.method === 'options') {
29
+ response.header(
30
+ 'Access-Control-Allow-Headers',
31
+ 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'
32
+ )
33
+ response.header(
34
+ 'Access-Control-Allow-Methods',
35
+ 'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT'
36
+ )
37
+ response.status(200)
38
+ return responseToLambdaResult(response.toResponse())
39
+ }
40
+
41
+ try {
42
+ await fetchData(request, response, {
43
+ singletonServices,
44
+ createSessionServices: createSessionServices as any,
45
+ })
46
+ } catch {
47
+ // Error should have already been handled by fetch
48
+ }
49
+
50
+ return responseToLambdaResult(response.toResponse())
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './http/index.js'
2
+ export * from './websocket/index.js'
3
+ export * from './queue/index.js'
@@ -0,0 +1 @@
1
+ export { runSQSQueueWorker } from './sqs-worker.js'
@@ -0,0 +1,97 @@
1
+ import { SQSBatchResponse, SQSEvent, SQSRecord } from 'aws-lambda'
2
+ import { runQueueJob } from '@pikku/core/queue'
3
+ import type {
4
+ CoreSingletonServices,
5
+ CreateSessionServices,
6
+ QueueJob,
7
+ QueueJobStatus,
8
+ } from '@pikku/core'
9
+
10
+ /**
11
+ * Enhanced version with additional SQS-specific features
12
+ */
13
+ export function mapSQSRecordToQueueJob(
14
+ record: SQSRecord,
15
+ maxAttempts?: number
16
+ ): QueueJob {
17
+ let data: any
18
+
19
+ try {
20
+ data = JSON.parse(record.body)
21
+ } catch {
22
+ data = record.body
23
+ }
24
+
25
+ if (!record.eventSourceARN) {
26
+ throw new Error('SQS record does not have eventSourceARN')
27
+ }
28
+
29
+ const queueName = record.eventSourceARN.split(':').pop()!
30
+
31
+ const job: QueueJob = {
32
+ queueName,
33
+ data,
34
+ id: record.messageId,
35
+ status: async () => 'active' as QueueJobStatus,
36
+ metadata: () => ({
37
+ processedAt: new Date(),
38
+ attemptsMade: parseInt(record.attributes.ApproximateReceiveCount) - 1,
39
+ maxAttempts,
40
+ result: undefined,
41
+ progress: 0,
42
+ createdAt: new Date(parseInt(record.attributes.SentTimestamp)),
43
+ completedAt: undefined,
44
+ failedAt: undefined,
45
+ error: undefined,
46
+ }),
47
+ waitForCompletion: async () => {
48
+ throw new Error(
49
+ 'SQS does not support waitForCompletion - jobs are fire-and-forget'
50
+ )
51
+ },
52
+ }
53
+ return job
54
+ }
55
+
56
+ /**
57
+ * Create an SQS handler that processes queue jobs using Pikku
58
+ */
59
+ export const runSQSQueueWorker = async (
60
+ singletonServices: CoreSingletonServices,
61
+ createSessionServices: CreateSessionServices<any, any, any> | undefined,
62
+ event: SQSEvent
63
+ ): Promise<SQSBatchResponse> => {
64
+ console.log(JSON.stringify(event, null, 2))
65
+ const jobs = event.Records.map(mapSQSRecordToQueueJob)
66
+
67
+ // Process all jobs in parallel
68
+ const results = await Promise.allSettled(
69
+ jobs.map((job) =>
70
+ runQueueJob({
71
+ singletonServices,
72
+ createSessionServices,
73
+ job,
74
+ })
75
+ )
76
+ )
77
+
78
+ // Log any failures but don't throw (SQS will handle retries)
79
+ results.forEach((result, index) => {
80
+ if (result.status === 'rejected') {
81
+ singletonServices.logger.error(
82
+ `Failed to process SQS message ${jobs[index]!.id}:`,
83
+ result.reason
84
+ )
85
+ }
86
+ })
87
+
88
+ return {
89
+ batchItemFailures: results
90
+ .map((result, index) => ({
91
+ result,
92
+ itemIdentifier: jobs[index]!.id || `message-${index}`,
93
+ }))
94
+ .filter(({ result }) => result.status === 'rejected')
95
+ .map(({ itemIdentifier }) => ({ itemIdentifier })),
96
+ }
97
+ }
@@ -0,0 +1,24 @@
1
+ import type { APIGatewayProxyEvent } from 'aws-lambda'
2
+
3
+ export const lambdaEventToRequest = (event: APIGatewayProxyEvent): Request => {
4
+ const url = new URL(event.path, `https://${event.headers.host}`)
5
+
6
+ if (event.queryStringParameters) {
7
+ for (const [key, value] of Object.entries(event.queryStringParameters)) {
8
+ if (value !== undefined) url.searchParams.set(key, value)
9
+ }
10
+ }
11
+
12
+ const body =
13
+ event.body && event.isBase64Encoded
14
+ ? Buffer.from(event.body, 'base64')
15
+ : event.body || undefined
16
+
17
+ return new Request(url.toString(), {
18
+ method: event.httpMethod,
19
+ headers: event.headers as HeadersInit,
20
+ body: ['GET', 'HEAD'].includes(event.httpMethod.toUpperCase())
21
+ ? undefined
22
+ : body,
23
+ })
24
+ }
@@ -0,0 +1,33 @@
1
+ import type { APIGatewayProxyResult } from 'aws-lambda'
2
+
3
+ export const responseToLambdaResult = async (
4
+ response: Response
5
+ ): Promise<APIGatewayProxyResult> => {
6
+ const headers: Record<string, string> = {}
7
+ response.headers.forEach((value, key) => {
8
+ headers[key.toLowerCase()] = value
9
+ })
10
+
11
+ let body: string
12
+ let isBase64Encoded = false
13
+
14
+ const contentType = response.headers.get('content-type') || ''
15
+ const isBinary = !/^text\/|application\/(json|javascript|xml)/.test(
16
+ contentType
17
+ )
18
+
19
+ const buffer = Buffer.from(await response.arrayBuffer())
20
+ if (isBinary) {
21
+ body = buffer.toString('base64')
22
+ isBase64Encoded = true
23
+ } else {
24
+ body = buffer.toString('utf-8')
25
+ }
26
+
27
+ return {
28
+ statusCode: response.status,
29
+ headers,
30
+ body,
31
+ isBase64Encoded,
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ export * from './websocket-message.js'
2
+ export * from './websocket-connect.js'
3
+ export * from './websocket-disconnect.js'
4
+ export * from './lambda-eventhub-service.js'
@@ -0,0 +1,53 @@
1
+ import { CoreUserSession } from '@pikku/core'
2
+ import {
3
+ ChannelStore,
4
+ PikkuAbstractChannelHandler,
5
+ PikkuChannelHandlerFactory,
6
+ } from '@pikku/core/channel'
7
+ import { sendMessage } from './utils.js'
8
+ import { ApiGatewayManagementApiClient } from '@aws-sdk/client-apigatewaymanagementapi'
9
+ import { Logger } from '@pikku/core/services'
10
+
11
+ class LambdaChannelHandler<
12
+ UserSession extends CoreUserSession = CoreUserSession,
13
+ OpeningData = unknown,
14
+ Out = unknown,
15
+ > extends PikkuAbstractChannelHandler<OpeningData, Out> {
16
+ constructor(
17
+ private logger: Logger,
18
+ private callbackAPI: ApiGatewayManagementApiClient,
19
+ channelId: string,
20
+ channelName: string,
21
+ openingData: OpeningData
22
+ ) {
23
+ super(channelId, channelName, openingData)
24
+ }
25
+
26
+ public async send(message: Out, isBinary?: boolean) {
27
+ if (isBinary) {
28
+ throw new Error('Binary data is not supported on serverless lambdas')
29
+ }
30
+ const data = JSON.stringify(message)
31
+ await sendMessage(this.logger, this.callbackAPI, this.channelId, data)
32
+ }
33
+ }
34
+
35
+ export const createLambdaChannelHandlerFactory = (
36
+ logger: Logger,
37
+ channelStore: ChannelStore,
38
+ callbackAPI: ApiGatewayManagementApiClient
39
+ ) => {
40
+ const factory: PikkuChannelHandlerFactory = (
41
+ channelId,
42
+ channelName,
43
+ openingData
44
+ ) =>
45
+ new LambdaChannelHandler(
46
+ logger,
47
+ callbackAPI,
48
+ channelId,
49
+ channelName,
50
+ openingData
51
+ )
52
+ return factory
53
+ }
@@ -0,0 +1,69 @@
1
+ import { ApiGatewayManagementApiClient } from '@aws-sdk/client-apigatewaymanagementapi'
2
+ import {
3
+ ChannelStore,
4
+ EventHubService,
5
+ EventHubStore,
6
+ } from '@pikku/core/channel'
7
+ import { getApiGatewayManagementApiClient, sendMessages } from './utils.js'
8
+ import { Logger } from '@pikku/core/services'
9
+ import { APIGatewayEvent } from 'aws-lambda'
10
+
11
+ export class LambdaEventHubService<EventTopics extends Record<string, any> = {}>
12
+ implements EventHubService<EventTopics>
13
+ {
14
+ private callbackAPI: ApiGatewayManagementApiClient
15
+
16
+ constructor(
17
+ private logger: Logger,
18
+ event: APIGatewayEvent,
19
+ private channelStore: ChannelStore,
20
+ private eventHubStore: EventHubStore<EventTopics>
21
+ ) {
22
+ this.callbackAPI = getApiGatewayManagementApiClient(logger, event)
23
+ }
24
+
25
+ async subscribe<T extends keyof EventTopics>(
26
+ topic: T,
27
+ channelId: string
28
+ ): Promise<void> {
29
+ await this.eventHubStore.subscribe(topic, channelId)
30
+ }
31
+
32
+ async unsubscribe<T extends keyof EventTopics>(
33
+ topic: T,
34
+ channelId: string
35
+ ): Promise<void> {
36
+ await this.eventHubStore.unsubscribe(topic, channelId)
37
+ }
38
+
39
+ async publish<T extends keyof EventTopics>(
40
+ topic: T,
41
+ channelId: string | null,
42
+ data: EventTopics[T],
43
+ isBinary?: boolean
44
+ ): Promise<void> {
45
+ const channelIds = await this.eventHubStore.getChannelIdsForTopic(
46
+ topic as string
47
+ )
48
+ if (channelId) {
49
+ await this.sendMessages(channelIds, channelId, data, isBinary)
50
+ }
51
+ }
52
+
53
+ private async sendMessages(
54
+ channelIds: string[],
55
+ fromChannelId: string,
56
+ data: EventTopics[keyof EventTopics],
57
+ isBinary?: boolean
58
+ ): Promise<void> {
59
+ await sendMessages(
60
+ this.logger,
61
+ this.channelStore,
62
+ this.callbackAPI,
63
+ fromChannelId,
64
+ channelIds,
65
+ data,
66
+ isBinary
67
+ )
68
+ }
69
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ ApiGatewayManagementApiClient,
3
+ PostToConnectionCommand,
4
+ } from '@aws-sdk/client-apigatewaymanagementapi'
5
+ import { Logger } from '@pikku/core/services'
6
+ import { APIGatewayEvent } from 'aws-lambda'
7
+ import { createLambdaChannelHandlerFactory } from './lambda-channel-handler.js'
8
+ import { ChannelStore } from '@pikku/core/channel'
9
+
10
+ export const sendMessage = async (
11
+ logger: Logger,
12
+ callbackAPI: ApiGatewayManagementApiClient,
13
+ ConnectionId: string,
14
+ Data: string
15
+ ): Promise<boolean> => {
16
+ try {
17
+ await callbackAPI.send(new PostToConnectionCommand({ ConnectionId, Data }))
18
+ return true
19
+ } catch (e: any) {
20
+ // TODO: We need to check if it's a 410 and remove the connection if it is
21
+ // Otherwise we need to log the error
22
+ logger.error(e.message)
23
+ return false
24
+ }
25
+ }
26
+
27
+ export const sendMessages = async (
28
+ logger: Logger,
29
+ channelStore: ChannelStore,
30
+ callbackAPI: ApiGatewayManagementApiClient,
31
+ fromChannelId: string,
32
+ channelIds: string[],
33
+ data: unknown,
34
+ isBinary?: boolean
35
+ ): Promise<void> => {
36
+ if (isBinary) {
37
+ throw new Error('Binary data is not supported on serverless lambdas')
38
+ }
39
+ const staleWebsockets: string[] = []
40
+ const Data = JSON.stringify(data)
41
+ await Promise.all(
42
+ channelIds.map(async (channelId) => {
43
+ if (channelId !== fromChannelId) {
44
+ const success = await sendMessage(logger, callbackAPI, channelId, Data)
45
+ if (!success) {
46
+ staleWebsockets.push(channelId)
47
+ }
48
+ }
49
+ })
50
+ )
51
+ if (staleWebsockets.length > 0) {
52
+ await channelStore.removeChannels(staleWebsockets)
53
+ }
54
+ }
55
+
56
+ export const getApiGatewayManagementApiClient = (
57
+ logger: Logger,
58
+ event: APIGatewayEvent
59
+ ) => {
60
+ let endpoint: string
61
+ if (process.env.IS_OFFLINE) {
62
+ logger.info('Assuming serverless offline mode due to IS_OFFLINE')
63
+ endpoint = `http://localhost:3001`
64
+ } else if (event.requestContext.domainName?.includes('amazonaws.com')) {
65
+ logger.info(
66
+ 'Assuming we are using the default endpoint mode due to amazonaws.com domain'
67
+ )
68
+ endpoint = `https://${event.requestContext.domainName}/${event.requestContext.stage}`
69
+ } else {
70
+ logger.info('Assuming we are using a custom domain mode')
71
+ endpoint = `https://${event.requestContext.domainName}`
72
+ }
73
+ return new ApiGatewayManagementApiClient({
74
+ apiVersion: '2018-11-29',
75
+ endpoint,
76
+ })
77
+ }
78
+
79
+ export const getServerlessDependencies = (
80
+ logger: Logger,
81
+ channelStore: ChannelStore,
82
+ event: APIGatewayEvent
83
+ ) => {
84
+ const channelId = event.requestContext.connectionId
85
+ if (!channelId) {
86
+ throw new Error('No connectionId found in requestContext')
87
+ }
88
+ const callbackAPI = getApiGatewayManagementApiClient(logger, event)
89
+ const channelHandlerFactory = createLambdaChannelHandlerFactory(
90
+ logger,
91
+ channelStore,
92
+ callbackAPI
93
+ )
94
+ return { channelId, callbackAPI, channelHandlerFactory, channelStore }
95
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ CoreSingletonServices,
3
+ CoreServices,
4
+ CoreUserSession,
5
+ PikkuFetchHTTPRequest,
6
+ PikkuFetchHTTPResponse,
7
+ } from '@pikku/core'
8
+ import { APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda'
9
+ import { responseToLambdaResult } from '../response-converter.js'
10
+ import { runChannelConnect } from '@pikku/core/channel/serverless'
11
+ import { getServerlessDependencies } from './utils.js'
12
+ import { WebsocketParams } from './websocket-types.js'
13
+ import { lambdaEventToRequest } from '../request-converter.js'
14
+
15
+ export const connectWebsocket = async <
16
+ SingletonServices extends CoreSingletonServices,
17
+ Services extends CoreServices<SingletonServices>,
18
+ UserSession extends CoreUserSession,
19
+ >(
20
+ event: APIGatewayEvent,
21
+ {
22
+ singletonServices,
23
+ createSessionServices,
24
+ channelStore,
25
+ }: WebsocketParams<SingletonServices, Services, UserSession>
26
+ ): Promise<APIGatewayProxyResult> => {
27
+ const runnerParams = getServerlessDependencies(
28
+ singletonServices.logger,
29
+ channelStore,
30
+ event
31
+ )
32
+ const request = new PikkuFetchHTTPRequest(lambdaEventToRequest(event))
33
+ const response = new PikkuFetchHTTPResponse()
34
+ await runChannelConnect({
35
+ ...runnerParams,
36
+ request,
37
+ response,
38
+ singletonServices: singletonServices as any,
39
+ createSessionServices: createSessionServices as any,
40
+ route: event.path || '/',
41
+ })
42
+ return responseToLambdaResult(response.toResponse())
43
+ }
@@ -0,0 +1,35 @@
1
+ import {
2
+ CoreSingletonServices,
3
+ CoreServices,
4
+ CoreUserSession,
5
+ } from '@pikku/core'
6
+ import { runChannelDisconnect } from '@pikku/core/channel/serverless'
7
+ import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
8
+
9
+ import { getServerlessDependencies } from './utils.js'
10
+ import { WebsocketParams } from './websocket-types.js'
11
+
12
+ export const disconnectWebsocket = async <
13
+ SingletonServices extends CoreSingletonServices,
14
+ Services extends CoreServices<SingletonServices>,
15
+ UserSession extends CoreUserSession,
16
+ >(
17
+ event: APIGatewayProxyEvent,
18
+ {
19
+ singletonServices,
20
+ createSessionServices,
21
+ channelStore,
22
+ }: WebsocketParams<SingletonServices, Services, UserSession>
23
+ ): Promise<APIGatewayProxyResult> => {
24
+ const runnerParams = getServerlessDependencies(
25
+ singletonServices.logger,
26
+ channelStore,
27
+ event
28
+ )
29
+ await runChannelDisconnect({
30
+ ...runnerParams,
31
+ singletonServices: singletonServices as any,
32
+ createSessionServices: createSessionServices as any,
33
+ })
34
+ return { statusCode: 200, body: '' }
35
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ CoreSingletonServices,
3
+ CoreServices,
4
+ CoreUserSession,
5
+ PikkuFetchHTTPResponse,
6
+ } from '@pikku/core'
7
+ import { runChannelMessage } from '@pikku/core/channel/serverless'
8
+ import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
9
+ import { getServerlessDependencies } from './utils.js'
10
+ import { WebsocketParams } from './websocket-types.js'
11
+ import { responseToLambdaResult } from '../response-converter.js'
12
+
13
+ export const processWebsocketMessage = async <
14
+ SingletonServices extends CoreSingletonServices,
15
+ Services extends CoreServices<SingletonServices>,
16
+ UserSession extends CoreUserSession,
17
+ >(
18
+ event: APIGatewayProxyEvent,
19
+ {
20
+ singletonServices,
21
+ createSessionServices,
22
+ channelStore,
23
+ }: WebsocketParams<SingletonServices, Services, UserSession>
24
+ ): Promise<APIGatewayProxyResult> => {
25
+ const runnerParams = getServerlessDependencies(
26
+ singletonServices.logger,
27
+ channelStore,
28
+ event
29
+ )
30
+ const response = new PikkuFetchHTTPResponse()
31
+ try {
32
+ const result = await runChannelMessage(
33
+ {
34
+ ...runnerParams,
35
+ singletonServices: singletonServices as any,
36
+ createSessionServices: createSessionServices as any,
37
+ },
38
+ event.body
39
+ )
40
+ if (result) {
41
+ // TODO: Support non json
42
+ response.json(result as any)
43
+ }
44
+ } catch (e) {
45
+ // Error should have already been handled by fetch
46
+ console.error(e)
47
+ }
48
+ return responseToLambdaResult(response.toResponse())
49
+ }
@@ -0,0 +1,21 @@
1
+ import {
2
+ CoreSingletonServices,
3
+ CoreServices,
4
+ CoreUserSession,
5
+ CreateSessionServices,
6
+ } from '@pikku/core'
7
+ import { ChannelStore } from '@pikku/core/channel'
8
+
9
+ export type WebsocketParams<
10
+ SingletonServices extends CoreSingletonServices,
11
+ Services extends CoreServices<SingletonServices>,
12
+ UserSession extends CoreUserSession,
13
+ > = {
14
+ channelStore: ChannelStore
15
+ singletonServices: SingletonServices
16
+ createSessionServices?: CreateSessionServices<
17
+ SingletonServices,
18
+ Services,
19
+ UserSession
20
+ >
21
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "module": "Node18",
6
+ "outDir": "dist",
7
+ "target": "esnext",
8
+ "declaration": true
9
+ },
10
+ "include": ["bin/**/*.ts", "src/**/*.ts"],
11
+ "exclude": ["**/*.test.ts", "node_modules", "bin/dist"],
12
+ "references": [
13
+ {
14
+ "path": "../../core/tsconfig.json"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/index.ts","./src/request-converter.ts","./src/response-converter.ts","./src/http/index.ts","./src/http/run-fetch.ts","./src/queue/index.ts","./src/queue/sqs-worker.ts","./src/websocket/index.ts","./src/websocket/lambda-channel-handler.ts","./src/websocket/lambda-eventhub-service.ts","./src/websocket/utils.ts","./src/websocket/websocket-connect.ts","./src/websocket/websocket-disconnect.ts","./src/websocket/websocket-message.ts","./src/websocket/websocket-types.ts"],"version":"5.8.3"}