-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Serverless LiveViewJS example code and workspace
- Loading branch information
Showing
22 changed files
with
2,463 additions
and
408 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
public/*.png | ||
public/*.gif | ||
public/*.jpg | ||
public/*.jpeg | ||
public/*.pdf | ||
public/js/ | ||
|
||
*.js | ||
!jest.config.js | ||
*.d.ts | ||
node_modules | ||
|
||
# CDK asset staging directory | ||
.cdk.staging | ||
cdk.out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
*.ts | ||
!*.d.ts | ||
|
||
# CDK asset staging directory | ||
.cdk.staging | ||
cdk.out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
engine-strict=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
node_modules | ||
.cache/ | ||
.env | ||
dist/ | ||
.parcel-cache/ | ||
.DS_Store | ||
coverage/ | ||
package-lock.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"printWidth": 120, | ||
"bracketSameLine": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# 🖼 LiveViewJS for AWS Lambda (NodeJS) | ||
|
||
The is a proof-of-concept that shows how you can host a "serverless" LiveViewJS application on AWS Lambda and API Gateway. The project loads the examples from the [LiveViewJS](https://liveviewjs.com) but you can adapt this to host your own LiveViewJS application. | ||
|
||
## Status | ||
This is a proof-of-concept and is NOT production ready for large volumes. In particular, the following issues need to be addressed: | ||
- API Gateway Websocket requests may not be handled in order that they were received (thus race conditions can occur) | ||
- LiveViewJS currently keeps LiveView state in memory which may break in the | ||
case multiple lambda functions attempt to handle requests from the same LiveView | ||
|
||
We can address these issue by storing LiveView state in a different data store (e.g. DynamoDB) and using a queue to ensure that requests are handled in order. | ||
|
||
## Summary AWS Architecture | ||
The set of AWS resources that are created are pretty simple: | ||
- API Gateway Websocket API passing requests to a Single Lambda function | ||
- API Gateway HTTP API passing requests to a Single Lambda function | ||
|
||
## Pre-requisites | ||
You should have an AWS account and have the AWS CLI installed and configured with your credentials. | ||
|
||
Run `npm install` to install dependencies. | ||
|
||
## Deploy | ||
This project uses AWS CDK to setup the infrastructure and deploy the code. | ||
|
||
Deploy to AWS Lambda using `cdk deploy [--profile YOUR_AWS_PROFILE]`. | ||
|
||
When the | ||
|
||
## Teardown | ||
After you are done with the project you can remove the stack from your AWS account by running: | ||
|
||
`cdk destroy [--profile YOUR_AWS_PROFILE]` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#!/usr/bin/env node | ||
import * as cdk from "aws-cdk-lib"; | ||
import "source-map-support/register"; | ||
import { LiveViewJSLambdaStack } from "../lib/liveviewjs-lambda-stack"; | ||
|
||
const app = new cdk.App(); | ||
new LiveViewJSLambdaStack(app, "LiveViewJSLambdaStack", { | ||
/* If you don't specify 'env', this stack will be environment-agnostic. | ||
* Account/Region-dependent features and context lookups will not work, | ||
* but a single synthesized template can be deployed anywhere. */ | ||
/* Uncomment the next line to specialize this stack for the AWS Account | ||
* and Region that are implied by the current CLI configuration. */ | ||
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, | ||
/* Uncomment the next line if you know exactly what Account and Region you | ||
* want to deploy the stack to. */ | ||
// env: { account: '123456789012', region: 'us-east-1' }, | ||
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"app": "npx ts-node --prefer-ts-exts bin/liveviewjs-lambda.ts", | ||
"watch": { | ||
"include": [ | ||
"**" | ||
], | ||
"exclude": [ | ||
"README.md", | ||
"cdk*.json", | ||
"**/*.d.ts", | ||
"**/*.js", | ||
"tsconfig.json", | ||
"package*.json", | ||
"yarn.lock", | ||
"node_modules", | ||
"test" | ||
] | ||
}, | ||
"context": { | ||
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, | ||
"@aws-cdk/core:stackRelativeExports": true, | ||
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true, | ||
"@aws-cdk/aws-lambda:recognizeVersionProps": true, | ||
"@aws-cdk/aws-lambda:recognizeLayerVersion": true, | ||
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, | ||
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, | ||
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, | ||
"@aws-cdk/core:checkSecretUsage": true, | ||
"@aws-cdk/aws-iam:minimizePolicies": true, | ||
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, | ||
"@aws-cdk/core:validateSnapshotRemovalPolicy": true, | ||
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, | ||
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, | ||
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, | ||
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true, | ||
"@aws-cdk/core:enablePartitionLiterals": true, | ||
"@aws-cdk/core:target-partitions": [ | ||
"aws", | ||
"aws-cn" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { HttpApi, HttpMethod, WebSocketApi, WebSocketStage } from "@aws-cdk/aws-apigatewayv2-alpha"; | ||
import { HttpLambdaIntegration, WebSocketLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; | ||
import { CfnOutput, Stack, StackProps } from "aws-cdk-lib"; | ||
import { PolicyStatement } from "aws-cdk-lib/aws-iam"; | ||
import { Runtime } from "aws-cdk-lib/aws-lambda"; | ||
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; | ||
import { Construct } from "constructs"; | ||
|
||
/** | ||
* The CDK Stack for LiveViewJS that deploys the HTTP and WebSocket API Gateway endpoints | ||
* along with the Lambda functions that handle the requests. | ||
*/ | ||
export class LiveViewJSLambdaStack extends Stack { | ||
constructor(scope: Construct, id: string, props?: StackProps) { | ||
super(scope, id, props); | ||
|
||
// Lambda function for Websocket requests | ||
const websocketLambdaFn = new NodejsFunction(this, "LiveViewJS WS Fn", { | ||
entry: "src/lambdas/websocket.ts", | ||
runtime: Runtime.NODEJS_16_X, | ||
}); | ||
|
||
// API Gateway for Websocket requests | ||
const webSocketApi = new WebSocketApi(this, "LiveViewJS WS APIG", { | ||
connectRouteOptions: { integration: new WebSocketLambdaIntegration("connect", websocketLambdaFn) }, | ||
disconnectRouteOptions: { integration: new WebSocketLambdaIntegration("disconnect", websocketLambdaFn) }, | ||
defaultRouteOptions: { integration: new WebSocketLambdaIntegration("default", websocketLambdaFn) }, | ||
}); | ||
// grant permissions for the Lambda function to send messages back to the client | ||
webSocketApi.grantManageConnections(websocketLambdaFn); | ||
|
||
// The stage for the Websocket API Gateway | ||
new WebSocketStage(this, "LiveViewJS WS APIG Stage", { | ||
webSocketApi, | ||
// the stageName must be "websocket" because that is part of the | ||
// URL that the LiveViewJS client will connect to | ||
stageName: "websocket", // | ||
autoDeploy: true, | ||
}); | ||
|
||
// Lambda function for HTTP requests | ||
const httpLambdaFn = new NodejsFunction(this, "LiveViewJS HTTP Fn", { | ||
entry: "src/lambdas/http.ts", | ||
runtime: Runtime.NODEJS_16_X, | ||
bundling: { | ||
commandHooks: { | ||
beforeBundling(inputDir: string, outputDir: string) { | ||
return [ | ||
// run esbuild to bundle the client-side JS | ||
`npm run build:client -w apps/lambda-examples`, | ||
// copy public folder to outputDir | ||
`cp -r ${inputDir}/apps/lambda-examples/public ${outputDir}`, | ||
]; | ||
}, | ||
afterBundling() { | ||
// no-op | ||
return []; | ||
}, | ||
beforeInstall() { | ||
// no-op | ||
return []; | ||
}, | ||
}, | ||
}, | ||
}); | ||
const arnToReadApiGateways = this.formatArn({ | ||
service: "apigateway", | ||
resource: "/apis", | ||
account: "", // arn requires empty account for some reason | ||
}); | ||
httpLambdaFn.addToRolePolicy( | ||
new PolicyStatement({ actions: ["apigateway:GET"], resources: [arnToReadApiGateways] }) | ||
); | ||
|
||
// API Gateway for HTTP requests | ||
const httpApi = new HttpApi(this, "LiveViewJS HTTP APIG"); | ||
// Route all requests to the Lambda function | ||
httpApi.addRoutes({ | ||
path: "/{proxy+}", | ||
methods: [HttpMethod.ANY], | ||
integration: new HttpLambdaIntegration("http", httpLambdaFn), | ||
}); | ||
|
||
// print out the LiveViewJS App URL | ||
new CfnOutput(this, "URL", { value: httpApi.url! }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"name": "@liveviewjs/lambda-examples", | ||
"version": "0.0.0", | ||
"private": true, | ||
"scripts": { | ||
"start": "ts-node ./src/example/autorun.ts", | ||
"build": "npm run build:client && npm run build:server", | ||
"build:client": "esbuild ./src/client/index.ts --bundle --outdir=public/js --platform=browser --format=esm --minify --sourcemap", | ||
"build:server": "esbuild ./src/example/index.ts --bundle --outdir=build --platform=node --format=cjs --minify --sourcemap", | ||
"clean": "rm -rf build; rm -rf dist", | ||
"dist": "npm run build", | ||
"format": "prettier --write '**/*.{ts,js,json,html,css}'" | ||
}, | ||
"bin": { | ||
"cdk": "bin/liveviewjs-lambda.ts" | ||
}, | ||
"keywords": [ | ||
"liveviewjs", | ||
"liveview", | ||
"phoenix", | ||
"typescript", | ||
"javascript", | ||
"lambda", | ||
"aws" | ||
], | ||
"author": "Donnie Flood <[email protected]>", | ||
"license": "MIT", | ||
"dependencies": { | ||
"@liveviewjs/examples": "*", | ||
"@liveviewjs/express": "*", | ||
"liveviewjs": "*", | ||
"nanoid": "^3.2.0" | ||
}, | ||
"devDependencies": { | ||
"@aws-cdk/aws-apigatewayv2-alpha": "^2.46.0-alpha.0", | ||
"@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.46.0-alpha.0", | ||
"@types/aws-lambda": "^8.10.108", | ||
"@types/node": "^18.7.8", | ||
"@types/nprogress": "^0.2.0", | ||
"@types/phoenix": "^1.5.4", | ||
"@types/phoenix_live_view": "^0.15.1", | ||
"aws-cdk": "^2.43.1", | ||
"aws-cdk-lib": "^2.43.1", | ||
"aws-lambda": "^1.0.7", | ||
"aws-sdk": "^2.1236.0", | ||
"esbuild": "^0.14.53", | ||
"nprogress": "^0.2.0", | ||
"phoenix": "^1.6.12", | ||
"phoenix_html": "^3.2.0", | ||
"phoenix_live_view": "^0.18.0", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^4.5.4" | ||
} | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import NProgress from "nprogress"; | ||
import { Socket } from "phoenix"; | ||
import "phoenix_html"; | ||
import { LiveSocket, ViewHook } from "phoenix_live_view"; | ||
|
||
/** | ||
* Define custom LiveView Hooks that can tap into browser events. | ||
* See: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook | ||
*/ | ||
let Hooks = { | ||
/** | ||
* This hook can be used by an input element to prevent input other than numbers. | ||
* e.g. <input type="text" phx-hook="NumberInput" /> | ||
*/ | ||
NumberInput: { | ||
mounted() { | ||
this.el.addEventListener("input", () => { | ||
// replace all non-numeric characters with empty string | ||
this.el.value = this.el.value.replace(/\D/g, ""); | ||
}); | ||
}, | ||
} as ViewHook, | ||
}; | ||
|
||
// WS_APIG_ID in the URL below will be replaced with the API Gateway ID at runtime | ||
// See: src/lambdas/http.ts route handler for "/js/index.js" | ||
const url = "wss://WS_APIG_ID.execute-api.us-west-2.amazonaws.com"; | ||
let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); | ||
let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks }); | ||
|
||
// Show progress bar on live navigation and form submits | ||
window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); | ||
window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); | ||
|
||
// connect if there are any LiveViews on the page | ||
liveSocket.connect(); | ||
|
||
// expose liveSocket on window for web console debug logs and latency simulation: | ||
// liveSocket.enableDebug(); | ||
// liveSocket.enableLatencySim(1000) | ||
(window as any).liveSocket = liveSocket; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { | ||
autocompleteLiveView, | ||
booksLiveView, | ||
counterLiveView, | ||
dashboardLiveView, | ||
decarbLiveView, | ||
helloToggleEmojiLiveView, | ||
jsCmdsLiveView, | ||
paginateLiveView, | ||
photosLiveView, | ||
printLiveView, | ||
rtCounterLiveView, | ||
searchLiveView, | ||
serversLiveView, | ||
sortLiveView, | ||
volumeLiveView, | ||
volunteerLiveView, | ||
xkcdLiveView, | ||
} from "@liveviewjs/examples"; | ||
import { LiveViewRouter } from "liveviewjs"; | ||
import { LambdaLiveViewServer } from "../lambda/server"; | ||
import { htmlPageTemplate, wrapperTemplate } from "./liveTemplates"; | ||
|
||
// LiveViewRouter that maps the path to the LiveView | ||
const router: LiveViewRouter = { | ||
"/autocomplete": autocompleteLiveView, | ||
"/decarbonize": decarbLiveView, | ||
"/prints": printLiveView, | ||
"/volume": volumeLiveView, | ||
"/paginate": paginateLiveView, | ||
"/dashboard": dashboardLiveView, | ||
"/search": searchLiveView, | ||
"/servers": serversLiveView, | ||
"/sort": sortLiveView, | ||
"/volunteers": volunteerLiveView, | ||
"/counter": counterLiveView, | ||
"/jscmds": jsCmdsLiveView, | ||
"/photos": photosLiveView, | ||
"/xkcd": xkcdLiveView, | ||
"/rtcounter": rtCounterLiveView, | ||
"/books": booksLiveView, | ||
"/helloToggle": helloToggleEmojiLiveView, | ||
}; | ||
|
||
// Configure the LambdaLiveViewServer which generates handlers for HTTP | ||
// and WebSocket requests from API Gateway and AWS Lambda | ||
export const liveView = new LambdaLiveViewServer( | ||
router, | ||
htmlPageTemplate, | ||
{ title: "Lambda Demo", suffix: " · LiveViewJS" }, | ||
{ | ||
serDeSigningSecret: "signingSecret", | ||
wrapperTemplate, | ||
} | ||
); |
Oops, something went wrong.