diff --git a/expressjs/package-lock.json b/expressjs/package-lock.json index 5a248fe..799c192 100644 --- a/expressjs/package-lock.json +++ b/expressjs/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@unleash/express-openapi": "^0.2.2", + "@wasmer/wasi": "^1.2.2", "axios": "^0.25.0", "cloudevents": "^6.0.4", "dotenv": "^14.3.2", @@ -1497,6 +1498,11 @@ "swagger-ui-dist": "^4.10.3" } }, + "node_modules/@wasmer/wasi": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@wasmer/wasi/-/wasi-1.2.2.tgz", + "integrity": "sha512-39ZB3gefOVhBmkhf7Ta79RRSV/emIV8LhdvcWhP/MOZEjMmtzoZWMzt7phdKj8CUXOze+AwbvGK60lKaKldn1w==" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -8327,6 +8333,11 @@ "swagger-ui-dist": "^4.10.3" } }, + "@wasmer/wasi": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@wasmer/wasi/-/wasi-1.2.2.tgz", + "integrity": "sha512-39ZB3gefOVhBmkhf7Ta79RRSV/emIV8LhdvcWhP/MOZEjMmtzoZWMzt7phdKj8CUXOze+AwbvGK60lKaKldn1w==" + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", diff --git a/expressjs/package.json b/expressjs/package.json index cdb8ee1..a0e41e6 100644 --- a/expressjs/package.json +++ b/expressjs/package.json @@ -38,6 +38,7 @@ "homepage": "https://github.com/openshift-knative/showcase#readme", "dependencies": { "@unleash/express-openapi": "^0.2.2", + "@wasmer/wasi": "^1.2.2", "axios": "^0.25.0", "cloudevents": "^6.0.4", "dotenv": "^14.3.2", diff --git a/expressjs/scripts/extract-webjar.js b/expressjs/scripts/extract-webjar.js index 76afb0d..5015fae 100644 --- a/expressjs/scripts/extract-webjar.js +++ b/expressjs/scripts/extract-webjar.js @@ -4,9 +4,10 @@ const chalk = require('chalk') const path = require('path') class Webjar { + /** * Webjar stores the information about the webjar extraction. - * + * * @param {Object} vars - The variables to use. * @param {string} vars.group - The group of the webjar. * @param {string} vars.artifact - The artifact of the webjar. @@ -46,7 +47,7 @@ const webjars = [ /** * Extracts all webjars. - */ + */ async function extractWebjars() { let ps = [] for (const webjar of webjars) { @@ -55,11 +56,17 @@ async function extractWebjars() { return await Promise.all(ps) } +/** + * @callback Log + * @param {string} message + * @param {...any} args + */ + /** * Creates a log function for the given webjar. - * - * @param {Webjar} webjar - * @returns {(message: string, ...args: any[]) => void} + * + * @param {Webjar} webjar + * @returns {Log} */ function createLog(webjar) { return (...message) => { @@ -69,8 +76,8 @@ function createLog(webjar) { /** * Extracts the webjar to the target directory. - * - * @param {Webjar} webjar + * + * @param {Webjar} webjar */ async function extractWebjar(webjar) { const log = createLog(webjar) @@ -88,7 +95,8 @@ async function extractWebjar(webjar) { zipEntries.forEach(async zipEntry => { // outputs zip entries information if (zipEntry.entryName.startsWith(webjar.source) && !zipEntry.isDirectory) { - const targetPath = zipEntry.entryName.replace(webjar.source, webjar.target) + const targetPath = zipEntry.entryName + .replace(webjar.source, webjar.target) log(`${chalk.yellow(zipEntry.entryName)} -> ${chalk.green(targetPath)}`) const targetDir = path.dirname(targetPath) diff --git a/expressjs/src/app.js b/expressjs/src/app.js index 74a76cb..e6eb792 100644 --- a/expressjs/src/app.js +++ b/expressjs/src/app.js @@ -17,7 +17,7 @@ const routes = { events: require('./routes/events/endpoint'), } -const loaders = app => { +const loaders = async app => { // Middleware Functions middleware.logging(app) middleware.public(app) @@ -30,16 +30,16 @@ const loaders = app => { routes.home(app) routes.info(app) routes.hello(app) - routes.events(app) + await routes.events(app) } -const createApp = () => { +const createApp = async () => { dotenv.config() const ex = express() // Start initializing - loaders(ex) + await loaders(ex) return ex } diff --git a/expressjs/src/routes/events/endpoint.js b/expressjs/src/routes/events/endpoint.js index c5763ee..af34bde 100644 --- a/expressjs/src/routes/events/endpoint.js +++ b/expressjs/src/routes/events/endpoint.js @@ -1,36 +1,90 @@ const { HTTP } = require('cloudevents') const openapi = require('../../lib/openapi') const EventStore = require('./store') +const { PrinterFactory } = require('./pretty-print') const devdata = require('./devdata') -const store = new EventStore() -devdata.forEach(event => store.add(event)) +const printerFactory = new PrinterFactory() -module.exports = async app => { - app.get('/events', streamDoc, (_, res) => { - res.set('Content-Type', 'text/event-stream') - res.set('Cache-Control', 'no-cache') - res.set('Connection', 'keep-alive') - res.set('X-SSE-Content-Type', 'application/cloudevents+json') - res.set('transfer-encoding', 'chunked') - res.flushHeaders() +/** + * @typedef {import('./pretty-print').Printer} Printer + * @typedef {import('express').Express} Express + * @typedef {import('express').Request} Request + * @typedef {import('express').Response} Response + */ - const stream = store.createStream(res) - stream.stream() - }) +/** + * @type {Printer} + */ +let printer - app.post('/events', eventDoc, (req, res) => { - try { - const ce = HTTP.toEvent({ headers: req.headers, body: req.body }) - ce.validate() - store.add(ce) - res.status(201).end() - } catch (err) { - console.error(err) - res.status(500) - res.json(err) - } - }) +/** + * @type {EventStore} + */ +let store + +/** + * Initializes the routes. + * + * @param {Express} app - the Express app + */ +async function events(app) { + printer = await printerFactory.create() + + app.get('/events', streamDoc, stream) + app.post('/', eventDoc, recv) + app.post('/events', eventDoc, recv) + + store = new EventStore() + devdata.forEach(event => recvEvent(event)) +} + +/** + * Streams all registered CloudEvents. + * + * @param {Request} _ + * @param {Response} res - the HTTP response + */ +function stream(_, res) { + res.set('Content-Type', 'text/event-stream') + res.set('Cache-Control', 'no-cache') + res.set('Connection', 'keep-alive') + res.set('X-SSE-Content-Type', 'application/cloudevents+json') + res.set('transfer-encoding', 'chunked') + res.flushHeaders() + + const str = store.createStream(res) + str.stream() +} + +/** + * Receives a CloudEvent from an HTTP request. + * @param {Request} req - the HTTP request + * @param {Response} res - the HTTP response + * @returns {void} + */ +function recv(req, res) { + try { + const ce = HTTP.toEvent({ headers: req.headers, body: req.body }) + recvEvent(ce) + res.status(201).end() + } catch (err) { + console.error(err) + res.status(500) + res.json(err) + } +} + +/** + * Receives a CloudEvent, logs, and stores it. + * + * @param {CloudEvent} ce - the CloudEvent to receive + */ +function recvEvent(ce) { + ce.validate() + store.add(ce) + const out = printer.print(ce) + console.log('Received:\n', out) } const streamDoc = openapi.path({ @@ -97,3 +151,5 @@ const eventDoc = openapi.path({ } } }) + +module.exports = events diff --git a/expressjs/src/routes/events/pretty-print.js b/expressjs/src/routes/events/pretty-print.js new file mode 100644 index 0000000..d922caa --- /dev/null +++ b/expressjs/src/routes/events/pretty-print.js @@ -0,0 +1,102 @@ +/* global WebAssembly */ + +const { WASI, init } = require('@wasmer/wasi') +const { HTTP } = require('cloudevents') +const fs = require('fs').promises +const path = require('path') + +class PrinterFactory { + + /** + * Creates a new Printer instance. + * + * @returns {Promise} - the new Printer instance + */ + async create() { + // Initialize WASI + await init() + const wasi = new WASI({ + args: ['cloudevents-pretty-print.wasm'], + env: {}, + }) + const wasm = path.join(__dirname, '../../../build/wasm/cloudevents-pretty-print.wasm') + const buf = new Uint8Array(await fs.readFile(wasm)) + const module = await WebAssembly.compile(buf) + + // Instantiate the WASI module + const instance = await wasi.instantiate(module, {}) + + return new Printer({ instance }) + } +} + +class Printer { + + /** + * Creates a new Printer instance. + * + * @param {Object} v - the params object + * @param {WebAssembly.Instance} v.instance - the WebAssembly instance + */ + constructor({ instance }) { + this.mem = instance.exports.memory + this.fn = instance.exports.pp_print + } + + /** + * Prints a CloudEvent into a human readable text. + * + * @param {CloudEvent} ce - the CloudEvent to print + * @returns {string} - the human readable text + */ + print(ce) { + const message = HTTP.structured(ce).body + + writeToMemory(message, this.mem) + + const rc = this.fn(0) + if (rc !== 0) { + throw new Error(`pp_print() returned ${rc}`) + } + + return readFromMemory(this.mem) + } +} + +/** + * Writes a string into the shared memory as a CString. + * + * @param {string} message - the string to write + * @param {WebAssembly.Memory} mem - the shared memory + */ +function writeToMemory(message, mem) { + const enc = new TextEncoder() + const view = new Uint8Array(mem.buffer) + const state = enc.encodeInto(message, view) + view[state.written] = 0 +} + +/** + * Reads a CString from the shared memory. + * + * @param {WebAssembly.Memory} mem - the shared memory + * @returns {string} - the string read from the shared memory + */ +function readFromMemory(mem) { + const view = new Uint8Array(mem.buffer) + let messageBytes = [] + for (let i = 0; i < view.length; i++) { + if (view[i] === 0) { + break + } + messageBytes.push(view[i]) + } + const dec = new TextDecoder('utf-8') + const res = dec.decode(new Uint8Array(messageBytes)) + return res +} + +module.exports = { + Printer, + PrinterFactory +} diff --git a/expressjs/test/middleware/health.test.js b/expressjs/test/middleware/health.test.js index e8d8335..1b6ec97 100644 --- a/expressjs/test/middleware/health.test.js +++ b/expressjs/test/middleware/health.test.js @@ -5,7 +5,7 @@ const { expect, describe, it } = require('@jest/globals') describe('Route', () => { const app = createApp() it('GET /health/ready', async () => { - const res = await request(app) + const res = await request(await app) .get('/health/ready') expect(res.status).toBe(200) expect(res.headers['content-type']).toMatch(/application\/json/) @@ -13,7 +13,7 @@ describe('Route', () => { }) it('GET /health/live', async () => { - const res = await request(app) + const res = await request(await app) .get('/health/live') expect(res.status).toBe(200) expect(res.headers['content-type']).toMatch(/application\/json/) diff --git a/expressjs/test/middleware/metrics.test.js b/expressjs/test/middleware/metrics.test.js index 664cf72..1bf17fa 100644 --- a/expressjs/test/middleware/metrics.test.js +++ b/expressjs/test/middleware/metrics.test.js @@ -4,7 +4,7 @@ const { expect, describe, it } = require('@jest/globals') describe('Route', () => { it('GET /metrics', async () => { - const app = createApp() + const app = await createApp() const res = await request(app) .get('/metrics') expect(res.status).toBe(200) diff --git a/expressjs/test/middleware/openapi.test.js b/expressjs/test/middleware/openapi.test.js index a22a067..54d21a1 100644 --- a/expressjs/test/middleware/openapi.test.js +++ b/expressjs/test/middleware/openapi.test.js @@ -5,14 +5,14 @@ const { expect, describe, it } = require('@jest/globals') describe('Route', () => { const app = createApp() it('GET /swagger-ui/', async () => { - const res = await request(app) + const res = await request(await app) .get('/swagger-ui/') expect(res.status).toBe(200) expect(res.headers['content-type']).toMatch(/text\/html/) }) it('GET /openapi.json', async () => { - const res = await request(app) + const res = await request(await app) .get('/openapi.json') expect(res.status).toBe(200) expect(res.headers['content-type']).toMatch(/application\/json/) diff --git a/expressjs/test/routes/events/endpoint.test.js b/expressjs/test/routes/events/endpoint.test.js index 9b02c9c..de836d6 100644 --- a/expressjs/test/routes/events/endpoint.test.js +++ b/expressjs/test/routes/events/endpoint.test.js @@ -7,8 +7,8 @@ const axios = require('axios').default const { expect, describe, it } = require('@jest/globals') describe('Route', () => { - const app = createApp() it('GET,POST /events', async () => { + const app = await createApp() await withServer(app, async port => { const messages = [] const endpoint = `http://localhost:${port}/events` @@ -46,7 +46,7 @@ const sendExampleEvent = async endpoint => { return ce } -const withServer = async (app, fn) => { +async function withServer(app, fn) { const port = await freePort() const listener = app.listen(port) await waitForExpect(async () => { diff --git a/expressjs/test/routes/hello/endpoint.test.js b/expressjs/test/routes/hello/endpoint.test.js index bc39e54..3002c66 100644 --- a/expressjs/test/routes/hello/endpoint.test.js +++ b/expressjs/test/routes/hello/endpoint.test.js @@ -16,7 +16,7 @@ describe('Route', () => { return [201, 'OK'] }) - const res = await request(app) + const res = await request(await app) .get('/hello') .query({ who: 'James' }) @@ -32,7 +32,7 @@ describe('Route', () => { }) it('GET /hello?who=nobody', async () => { - const res = await request(app) + const res = await request(await app) .get('/hello') .query({ who: 'nobody' }) diff --git a/expressjs/test/routes/home/endpoint.test.js b/expressjs/test/routes/home/endpoint.test.js index 220a9f2..cdee454 100644 --- a/expressjs/test/routes/home/endpoint.test.js +++ b/expressjs/test/routes/home/endpoint.test.js @@ -5,7 +5,7 @@ const { expect, describe, it } = require('@jest/globals') describe('Route', () => { const app = createApp() it('GET /', async () => { - const res = await request(app).get('/') + const res = await request(await app).get('/') expect(res.status).toBe(200) expect(res.headers['content-type']).toMatch(/application\/json/) @@ -16,7 +16,7 @@ describe('Route', () => { }) it('GET / as Browser', async () => { - const res = await request(app) + const res = await request(await app) .get('/') .set('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36') diff --git a/expressjs/test/routes/info/endpoint.test.js b/expressjs/test/routes/info/endpoint.test.js index 3e5d10e..d976199 100644 --- a/expressjs/test/routes/info/endpoint.test.js +++ b/expressjs/test/routes/info/endpoint.test.js @@ -3,8 +3,8 @@ const createApp = require('../../../src/app') const { expect, describe, it } = require('@jest/globals') describe('Route', () => { - const app = createApp() it('GET /info', async () => { + const app = await createApp() const res = await request(app) .get('/info')