@pikku/lambda 0.6.3
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 +42 -0
- package/README.md +3 -0
- package/dist/http/anywhere-http.d.ts +3 -0
- package/dist/http/anywhere-http.js +13 -0
- package/dist/http/anywhere-http.js.map +1 -0
- package/dist/http/cors-http.d.ts +3 -0
- package/dist/http/cors-http.js +35 -0
- package/dist/http/cors-http.js.map +1 -0
- package/dist/http/corsless-http.d.ts +3 -0
- package/dist/http/corsless-http.js +9 -0
- package/dist/http/corsless-http.js.map +1 -0
- package/dist/http/general-http-handler.d.ts +5 -0
- package/dist/http/general-http-handler.js +28 -0
- package/dist/http/general-http-handler.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +4 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/pikku-api-gateway-lambda-request.d.ts +12 -0
- package/dist/pikku-api-gateway-lambda-request.js +24 -0
- package/dist/pikku-api-gateway-lambda-request.js.map +1 -0
- package/dist/pikku-api-gateway-lambda-response.d.ts +15 -0
- package/dist/pikku-api-gateway-lambda-response.js +32 -0
- package/dist/pikku-api-gateway-lambda-response.js.map +1 -0
- package/dist/websocket/index.d.ts +4 -0
- package/dist/websocket/index.js +5 -0
- package/dist/websocket/index.js.map +1 -0
- package/dist/websocket/lambda-channel-handler.d.ts +4 -0
- package/dist/websocket/lambda-channel-handler.js +29 -0
- package/dist/websocket/lambda-channel-handler.js.map +1 -0
- package/dist/websocket/lambda-eventhub-service.d.ts +14 -0
- package/dist/websocket/lambda-eventhub-service.js +27 -0
- package/dist/websocket/lambda-eventhub-service.js.map +1 -0
- package/dist/websocket/utils.d.ts +13 -0
- package/dist/websocket/utils.js +59 -0
- package/dist/websocket/utils.js.map +1 -0
- package/dist/websocket/websocket-connect.d.ts +4 -0
- package/dist/websocket/websocket-connect.js +19 -0
- package/dist/websocket/websocket-connect.js.map +1 -0
- package/dist/websocket/websocket-disconnect.d.ts +4 -0
- package/dist/websocket/websocket-disconnect.js +12 -0
- package/dist/websocket/websocket-disconnect.js.map +1 -0
- package/dist/websocket/websocket-message.d.ts +4 -0
- package/dist/websocket/websocket-message.js +22 -0
- package/dist/websocket/websocket-message.js.map +1 -0
- package/dist/websocket/websocket-types.d.ts +7 -0
- package/dist/websocket/websocket-types.js +2 -0
- package/dist/websocket/websocket-types.js.map +1 -0
- package/package.json +37 -0
- package/run-tests.sh +53 -0
- package/src/http/anywhere-http.ts +39 -0
- package/src/http/cors-http.ts +70 -0
- package/src/http/corsless-http.ts +33 -0
- package/src/http/general-http-handler.ts +59 -0
- package/src/http/index.ts +3 -0
- package/src/index.ts +1 -0
- package/src/pikku-api-gateway-lambda-request.ts +31 -0
- package/src/pikku-api-gateway-lambda-response.ts +41 -0
- package/src/websocket/index.ts +4 -0
- package/src/websocket/lambda-channel-handler.ts +63 -0
- package/src/websocket/lambda-eventhub-service.ts +59 -0
- package/src/websocket/utils.ts +95 -0
- package/src/websocket/websocket-connect.ts +41 -0
- package/src/websocket/websocket-disconnect.ts +35 -0
- package/src/websocket/websocket-message.ts +48 -0
- package/src/websocket/websocket-types.ts +22 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pikku/lambda",
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "yasser.fadl@gmail.com",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"tsc": "tsc",
|
|
11
|
+
"build:esm": "tsc -b",
|
|
12
|
+
"build": "yarn build:esm",
|
|
13
|
+
"release": "npm run build && npm test",
|
|
14
|
+
"ncu": "ncu",
|
|
15
|
+
"test": "bash run-tests.sh",
|
|
16
|
+
"test:watch": "bash run-tests.sh --watch",
|
|
17
|
+
"test:coverage": "bash run-tests.sh --coverage"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.js",
|
|
21
|
+
"./http": "./dist/http/index.js",
|
|
22
|
+
"./websocket": "./dist/websocket/index.js"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@aws-sdk/client-apigatewaymanagementapi": "^3.713.0",
|
|
26
|
+
"@pikku/core": "^0.6.7",
|
|
27
|
+
"@types/aws-lambda": "^8.10.146"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@aws-sdk/client-apigatewaymanagementapi": "^3.713.0",
|
|
31
|
+
"@types/aws-lambda": "^8.10.146",
|
|
32
|
+
"typescript": "^5.7.2"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/run-tests.sh
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Enable nullglob to handle cases where no files match the pattern
|
|
4
|
+
shopt -s nullglob
|
|
5
|
+
|
|
6
|
+
# Initialize variables for options
|
|
7
|
+
watch_mode=false
|
|
8
|
+
coverage_mode=false
|
|
9
|
+
|
|
10
|
+
# Parse command-line options
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case $1 in
|
|
13
|
+
--watch)
|
|
14
|
+
watch_mode=true
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
--coverage)
|
|
18
|
+
coverage_mode=true
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
*)
|
|
22
|
+
echo "Unknown option: $1"
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
# Define the pattern to match your test files
|
|
29
|
+
pattern="src/*.test.ts"
|
|
30
|
+
|
|
31
|
+
# Expand the pattern into an array of files
|
|
32
|
+
files=($(find src -type f -name "*.test.ts"))
|
|
33
|
+
|
|
34
|
+
# Check if any files matched the pattern
|
|
35
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
36
|
+
echo "No test files found matching pattern: $pattern"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Construct the node command
|
|
41
|
+
node_cmd="node --import tsx --test"
|
|
42
|
+
|
|
43
|
+
# Append options based on flags
|
|
44
|
+
if [ "$watch_mode" = true ]; then
|
|
45
|
+
node_cmd="$node_cmd --watch"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [ "$coverage_mode" = true ]; then
|
|
49
|
+
node_cmd="$node_cmd --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Execute the node command with the expanded list of files
|
|
53
|
+
$node_cmd "${files[@]}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CoreSingletonServices,
|
|
3
|
+
CoreServices,
|
|
4
|
+
CreateSessionServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
} from '@pikku/core'
|
|
7
|
+
import { APIGatewayProxyEvent } from 'aws-lambda'
|
|
8
|
+
import { generalHTTPHandler } from './general-http-handler.js'
|
|
9
|
+
import { PikkuAPIGatewayLambdaRequest } from '../pikku-api-gateway-lambda-request.js'
|
|
10
|
+
import { PikkuAPIGatewayLambdaResponse } from '../pikku-api-gateway-lambda-response.js'
|
|
11
|
+
|
|
12
|
+
export const anywhereHTTP = async <
|
|
13
|
+
SingletonServices extends CoreSingletonServices,
|
|
14
|
+
Services extends CoreServices<SingletonServices>,
|
|
15
|
+
>(
|
|
16
|
+
event: APIGatewayProxyEvent,
|
|
17
|
+
singletonServices: SingletonServices,
|
|
18
|
+
createSessionServices: CreateSessionServices<
|
|
19
|
+
SingletonServices,
|
|
20
|
+
Services,
|
|
21
|
+
CoreUserSession
|
|
22
|
+
>
|
|
23
|
+
) => {
|
|
24
|
+
const request = new PikkuAPIGatewayLambdaRequest(event)
|
|
25
|
+
const response = new PikkuAPIGatewayLambdaResponse()
|
|
26
|
+
if (event.headers.origin) {
|
|
27
|
+
response.setHeader(
|
|
28
|
+
'Access-Control-Allow-Origin',
|
|
29
|
+
event.headers.origin.toString()
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
response.setHeader('Access-Control-Allow-Credentials', true)
|
|
33
|
+
return await generalHTTPHandler(
|
|
34
|
+
singletonServices,
|
|
35
|
+
createSessionServices,
|
|
36
|
+
request,
|
|
37
|
+
response
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CoreSingletonServices,
|
|
3
|
+
CoreServices,
|
|
4
|
+
CreateSessionServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
} from '@pikku/core'
|
|
7
|
+
import type { APIGatewayProxyEvent } from 'aws-lambda'
|
|
8
|
+
import type { Logger } from '@pikku/core/services'
|
|
9
|
+
|
|
10
|
+
import { generalHTTPHandler } from './general-http-handler.js'
|
|
11
|
+
import { InvalidOriginError } from '@pikku/core/errors'
|
|
12
|
+
import { PikkuAPIGatewayLambdaResponse } from '../pikku-api-gateway-lambda-response.js'
|
|
13
|
+
import { PikkuAPIGatewayLambdaRequest } from '../pikku-api-gateway-lambda-request.js'
|
|
14
|
+
|
|
15
|
+
const validateOrigin = (
|
|
16
|
+
allowsOrigins: string[],
|
|
17
|
+
logger: Logger,
|
|
18
|
+
event: APIGatewayProxyEvent
|
|
19
|
+
): string => {
|
|
20
|
+
const origin = event.headers.origin
|
|
21
|
+
if (!origin || allowsOrigins.every((domain) => !origin.includes(domain))) {
|
|
22
|
+
logger.error(`
|
|
23
|
+
CORS Error
|
|
24
|
+
- Recieved from origin: ${origin}
|
|
25
|
+
- Expected domain(s): ${allowsOrigins.join(', ')}
|
|
26
|
+
- Host: ${event.headers.host}
|
|
27
|
+
- Path: ${event.path}
|
|
28
|
+
- Headers: ${JSON.stringify(event.headers, null, '\t')}
|
|
29
|
+
`)
|
|
30
|
+
throw new InvalidOriginError()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return origin
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const corsHTTP = async <
|
|
37
|
+
SingletonServices extends CoreSingletonServices,
|
|
38
|
+
Services extends CoreServices<SingletonServices>,
|
|
39
|
+
UserSession extends CoreUserSession,
|
|
40
|
+
>(
|
|
41
|
+
event: APIGatewayProxyEvent,
|
|
42
|
+
allowedOrigins: string[],
|
|
43
|
+
singletonServices: SingletonServices,
|
|
44
|
+
createSessionServices: CreateSessionServices<
|
|
45
|
+
SingletonServices,
|
|
46
|
+
Services,
|
|
47
|
+
UserSession
|
|
48
|
+
>
|
|
49
|
+
) => {
|
|
50
|
+
const request = new PikkuAPIGatewayLambdaRequest(event)
|
|
51
|
+
const response = new PikkuAPIGatewayLambdaResponse()
|
|
52
|
+
|
|
53
|
+
let origin: string | false = false
|
|
54
|
+
try {
|
|
55
|
+
origin = validateOrigin(allowedOrigins, singletonServices.logger, event)
|
|
56
|
+
} catch {
|
|
57
|
+
response.setStatus(400)
|
|
58
|
+
response.setJson({ error: 'error.invalid_origin' })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
response.setHeader('Access-Control-Allow-Origin', origin)
|
|
62
|
+
response.setHeader('Access-Control-Allow-Credentials', true)
|
|
63
|
+
|
|
64
|
+
return await generalHTTPHandler(
|
|
65
|
+
singletonServices,
|
|
66
|
+
createSessionServices,
|
|
67
|
+
request,
|
|
68
|
+
response
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { APIGatewayProxyEvent } from 'aws-lambda'
|
|
2
|
+
import {
|
|
3
|
+
CoreServices,
|
|
4
|
+
CoreSingletonServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
CreateSessionServices,
|
|
7
|
+
} from '@pikku/core'
|
|
8
|
+
import { generalHTTPHandler } from './general-http-handler.js'
|
|
9
|
+
import { PikkuAPIGatewayLambdaRequest } from '../pikku-api-gateway-lambda-request.js'
|
|
10
|
+
import { PikkuAPIGatewayLambdaResponse } from '../pikku-api-gateway-lambda-response.js'
|
|
11
|
+
|
|
12
|
+
export const corslessHTTP = async <
|
|
13
|
+
SingletonServices extends CoreSingletonServices,
|
|
14
|
+
Services extends CoreServices<SingletonServices>,
|
|
15
|
+
UserSession extends CoreUserSession,
|
|
16
|
+
>(
|
|
17
|
+
event: APIGatewayProxyEvent,
|
|
18
|
+
singletonServices: SingletonServices,
|
|
19
|
+
createSessionServices: CreateSessionServices<
|
|
20
|
+
SingletonServices,
|
|
21
|
+
Services,
|
|
22
|
+
UserSession
|
|
23
|
+
>
|
|
24
|
+
) => {
|
|
25
|
+
const request = new PikkuAPIGatewayLambdaRequest(event)
|
|
26
|
+
const response = new PikkuAPIGatewayLambdaResponse()
|
|
27
|
+
return await generalHTTPHandler(
|
|
28
|
+
singletonServices,
|
|
29
|
+
createSessionServices,
|
|
30
|
+
request,
|
|
31
|
+
response
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CoreSingletonServices,
|
|
3
|
+
CoreServices,
|
|
4
|
+
CreateSessionServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
} from '@pikku/core'
|
|
7
|
+
import { runHTTPRoute } from '@pikku/core/http'
|
|
8
|
+
import { APIGatewayProxyResult } from 'aws-lambda'
|
|
9
|
+
import { PikkuAPIGatewayLambdaRequest } from '../pikku-api-gateway-lambda-request.js'
|
|
10
|
+
import { PikkuAPIGatewayLambdaResponse } from '../pikku-api-gateway-lambda-response.js'
|
|
11
|
+
|
|
12
|
+
export const generalHTTPHandler = 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
|
+
request: PikkuAPIGatewayLambdaRequest,
|
|
24
|
+
response: PikkuAPIGatewayLambdaResponse
|
|
25
|
+
): Promise<APIGatewayProxyResult> => {
|
|
26
|
+
if (request.getMethod() === 'options') {
|
|
27
|
+
response.setHeader(
|
|
28
|
+
'Access-Control-Allow-Headers',
|
|
29
|
+
'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'
|
|
30
|
+
)
|
|
31
|
+
response.setHeader(
|
|
32
|
+
'Access-Control-Allow-Methods',
|
|
33
|
+
'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT'
|
|
34
|
+
)
|
|
35
|
+
response.setStatus(200)
|
|
36
|
+
response.setJson({})
|
|
37
|
+
return response.getLambdaResponse()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (request.getPath().includes('health-check')) {
|
|
41
|
+
response.setStatus(200)
|
|
42
|
+
return response.getLambdaResponse()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await runHTTPRoute({
|
|
47
|
+
request,
|
|
48
|
+
response,
|
|
49
|
+
singletonServices,
|
|
50
|
+
createSessionServices: createSessionServices as any,
|
|
51
|
+
route: request.getPath(),
|
|
52
|
+
method: request.getMethod() as any,
|
|
53
|
+
})
|
|
54
|
+
} catch {
|
|
55
|
+
// Error should have already been handled by runHTTPRoute
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response.getLambdaResponse()
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default null
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent } from 'aws-lambda'
|
|
2
|
+
import type { HTTPMethod, PikkuQuery } from '@pikku/core/http'
|
|
3
|
+
import { PikkuHTTPAbstractRequest } from '@pikku/core/http/pikku-http-abstract-request'
|
|
4
|
+
|
|
5
|
+
export class PikkuAPIGatewayLambdaRequest extends PikkuHTTPAbstractRequest {
|
|
6
|
+
constructor(protected event: APIGatewayProxyEvent) {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public getPath() {
|
|
11
|
+
// TODO: The path can be undefined, types are invalid
|
|
12
|
+
return this.event.path || '/'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public getMethod() {
|
|
16
|
+
return this.event.httpMethod.toLowerCase() as HTTPMethod
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async getBody() {
|
|
20
|
+
return this.event.body
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getHeader(headerName: string): string | undefined {
|
|
24
|
+
return this.event.headers[headerName]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public getQuery() {
|
|
28
|
+
// TODO: If query isn't defined, it should be an empty object
|
|
29
|
+
return (this.event.queryStringParameters as PikkuQuery) || {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { JSONValue } from '@pikku/core'
|
|
2
|
+
import { PikkuHTTPAbstractResponse } from '@pikku/core/http/pikku-http-abstract-response'
|
|
3
|
+
import { APIGatewayProxyResult } from 'aws-lambda'
|
|
4
|
+
|
|
5
|
+
export class PikkuAPIGatewayLambdaResponse extends PikkuHTTPAbstractResponse {
|
|
6
|
+
public headers: Record<string, string> = {}
|
|
7
|
+
private status: number = 200
|
|
8
|
+
private body: any | undefined
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
super()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public getLambdaResponse(): APIGatewayProxyResult {
|
|
15
|
+
return {
|
|
16
|
+
headers: this.headers,
|
|
17
|
+
statusCode: this.status,
|
|
18
|
+
body: this.body,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public setStatus(status: number): void {
|
|
23
|
+
this.status = status
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public setHeader(name: string, value: string | boolean | string[]): void {
|
|
27
|
+
this.headers[name] = value.toString()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public setJson(value: JSONValue): void {
|
|
31
|
+
this.body = JSON.stringify(value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public setResponse(response: string | Buffer): void {
|
|
35
|
+
this.body = response
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public setRedirect(path: string, status: number) {
|
|
39
|
+
throw new Error('Method not implemented.')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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<UserSession, OpeningData, Out> {
|
|
16
|
+
constructor(
|
|
17
|
+
private logger: Logger,
|
|
18
|
+
userSession: UserSession | undefined,
|
|
19
|
+
private channelStore: ChannelStore,
|
|
20
|
+
private callbackAPI: ApiGatewayManagementApiClient,
|
|
21
|
+
channelId: string,
|
|
22
|
+
channelName: string,
|
|
23
|
+
openingData: OpeningData
|
|
24
|
+
) {
|
|
25
|
+
super(channelId, channelName, userSession, openingData)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async setUserSession(userSession: UserSession): Promise<void> {
|
|
29
|
+
this.userSession = userSession
|
|
30
|
+
await this.channelStore.setUserSession(this.channelId, userSession)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async send(message: Out, isBinary?: boolean) {
|
|
34
|
+
if (isBinary) {
|
|
35
|
+
throw new Error('Binary data is not supported on serverless lambdas')
|
|
36
|
+
}
|
|
37
|
+
const data = JSON.stringify(message)
|
|
38
|
+
await sendMessage(this.logger, this.callbackAPI, this.channelId, data)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const createLambdaChannelHandlerFactory = (
|
|
43
|
+
logger: Logger,
|
|
44
|
+
channelStore: ChannelStore,
|
|
45
|
+
callbackAPI: ApiGatewayManagementApiClient
|
|
46
|
+
) => {
|
|
47
|
+
const factory: PikkuChannelHandlerFactory = (
|
|
48
|
+
channelId,
|
|
49
|
+
channelName,
|
|
50
|
+
openingData,
|
|
51
|
+
userSession
|
|
52
|
+
) =>
|
|
53
|
+
new LambdaChannelHandler(
|
|
54
|
+
logger,
|
|
55
|
+
userSession,
|
|
56
|
+
channelStore,
|
|
57
|
+
callbackAPI,
|
|
58
|
+
channelId,
|
|
59
|
+
channelName,
|
|
60
|
+
openingData
|
|
61
|
+
)
|
|
62
|
+
return factory
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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<Out = unknown>
|
|
12
|
+
implements EventHubService<Out>
|
|
13
|
+
{
|
|
14
|
+
private callbackAPI: ApiGatewayManagementApiClient
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private logger: Logger,
|
|
18
|
+
event: APIGatewayEvent,
|
|
19
|
+
private channelStore: ChannelStore,
|
|
20
|
+
private eventHubStore: EventHubStore
|
|
21
|
+
) {
|
|
22
|
+
this.callbackAPI = getApiGatewayManagementApiClient(logger, event)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async subscribe(topic: string, channelId: string): Promise<void> {
|
|
26
|
+
await this.eventHubStore.subscribe(topic, channelId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async unsubscribe(topic: string, channelId: string): Promise<void> {
|
|
30
|
+
await this.eventHubStore.unsubscribe(topic, channelId)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async publish(
|
|
34
|
+
topic: string,
|
|
35
|
+
fromChannelId: string,
|
|
36
|
+
data: Out,
|
|
37
|
+
isBinary?: boolean
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const channelIds = await this.eventHubStore.getChannelIdsForTopic(topic)
|
|
40
|
+
await this.sendMessages(channelIds, fromChannelId, data)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async sendMessages(
|
|
44
|
+
channelIds: string[],
|
|
45
|
+
fromChannelId: string,
|
|
46
|
+
data: Out,
|
|
47
|
+
isBinary?: boolean
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await sendMessages(
|
|
50
|
+
this.logger,
|
|
51
|
+
this.channelStore,
|
|
52
|
+
this.callbackAPI,
|
|
53
|
+
fromChannelId,
|
|
54
|
+
channelIds,
|
|
55
|
+
data,
|
|
56
|
+
isBinary
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CoreSingletonServices,
|
|
3
|
+
CoreServices,
|
|
4
|
+
CoreUserSession,
|
|
5
|
+
} from '@pikku/core'
|
|
6
|
+
import { APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda'
|
|
7
|
+
import { PikkuAPIGatewayLambdaResponse } from '../pikku-api-gateway-lambda-response.js'
|
|
8
|
+
import { runChannelConnect } from '@pikku/core/channel/serverless'
|
|
9
|
+
import { getServerlessDependencies } from './utils.js'
|
|
10
|
+
import { WebsocketParams } from './websocket-types.js'
|
|
11
|
+
import { PikkuAPIGatewayLambdaRequest } from '../pikku-api-gateway-lambda-request.js'
|
|
12
|
+
|
|
13
|
+
export const connectWebsocket = async <
|
|
14
|
+
SingletonServices extends CoreSingletonServices,
|
|
15
|
+
Services extends CoreServices<SingletonServices>,
|
|
16
|
+
UserSession extends CoreUserSession,
|
|
17
|
+
>(
|
|
18
|
+
event: APIGatewayEvent,
|
|
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 request = new PikkuAPIGatewayLambdaRequest(event)
|
|
31
|
+
const response = new PikkuAPIGatewayLambdaResponse()
|
|
32
|
+
await runChannelConnect({
|
|
33
|
+
...runnerParams,
|
|
34
|
+
request,
|
|
35
|
+
response,
|
|
36
|
+
singletonServices: singletonServices as any,
|
|
37
|
+
createSessionServices: createSessionServices as any,
|
|
38
|
+
route: event.path || '/',
|
|
39
|
+
})
|
|
40
|
+
return response.getLambdaResponse()
|
|
41
|
+
}
|
|
@@ -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
|
+
}
|