Skip to content
This repository was archived by the owner on Nov 22, 2024. It is now read-only.

feat(socket-engine): introduce package #999

Merged
merged 2 commits into from
Aug 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions WORKSPACE
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
workspace(name = "nguniversal")
http_archive(
name = "build_bazel_rules_nodejs",
url = "https://github.com/bazelbuild/rules_nodejs/archive/0.9.1.zip",
strip_prefix = "rules_nodejs-0.9.1",
sha256 = "6139762b62b37c1fd171d7f22aa39566cb7dc2916f0f801d505a9aaf118c117f",
# TODO: upgrade once https://github.com/bazelbuild/rules_nodejs/issues/218#issuecomment-395826361 is released
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran into some other issues with this change, will nbeed a full set of updates later

url = "https://github.com/bazelbuild/rules_nodejs/archive/0.8.0.zip",
strip_prefix = "rules_nodejs-0.8.0",
)

http_archive(
Expand Down
46 changes: 46 additions & 0 deletions modules/socket-engine/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
load("//tools:defaults.bzl", "ts_library", "ng_module", "ng_package")

package(default_visibility = ["//visibility:public"])

load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")

ng_module(
name = "socket-engine",
srcs = glob([
"*.ts",
"src/**/*.ts",
]),
module_name = "@nguniversal/socket-engine",
deps = [
"//modules/common",
],
)

ng_package(
name = "npm_package",
srcs = [
":package.json",
],
entry_point = "modules/socket-engine/index.js",
readme_md = ":README.md",
deps = [
":socket-engine",
"//modules/common",
],
)

ts_library(
name = "unit_test_lib",
testonly = True,
srcs = glob([
"spec/**/*.spec.ts",
]),
deps = [
":socket-engine",
],
)

jasmine_node_test(
name = "unit_test",
srcs = [":unit_test_lib"],
)
44 changes: 44 additions & 0 deletions modules/socket-engine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Angular Universal Socket Engine

Framework and Platform agnostic Angular Universal rendering.

## Usage Server

`npm install @nguniversal/socket-engine @nguniversal/common --save`

```js
const socketEngine = require('@nguniversal/socket-engine');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');

socketEngine.startSocketEngine(AppServerModuleNgFactory);
```
This will the socket engine which internally hosts a TCP Socket server.
The default port is `9090` and host of `localhost`
You may want to leave this as a plain `.js` file since it is so simple and to make deploying it easier, but it can be easily transpiled from Typescript.

## Usage Client

Your client can be whatever language, framework or platform you like.
As long as it can connect to a TCP Socket (which all frameworks can) then you're good to go.

This example will use JS for simplicity
```typescript
import * as net from 'net';

const client = net.createConnection(9090, 'localhost', () => {
console.log('connected to SSR server');
});

client.on('data', data => {
const res = JSON.parse(data.toString()) as SocketEngineResponse;
expect(res.id).toEqual(1);
expect(res.html).toEqual(template);
server.close();
done();
});

const renderOptions = {id: 1, url: '/path', document: '<app-root></app-root>'} as SocketEngineRenderOptions;
client.write(JSON.stringify(renderOptions));
```
8 changes: 8 additions & 0 deletions modules/socket-engine/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './public_api';
26 changes: 26 additions & 0 deletions modules/socket-engine/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@nguniversal/socket-engine",
"version": "0.0.0-PLACEHOLDER",
"description": "Socket Engine for running Server Angular Apps",
"license": "MIT",
"keywords": [
"socket",
"ssr",
"universal"
],
"peerDependencies": {
"@nguniversal/common": "0.0.0-PLACEHOLDER",
"@angular/core": "NG_VERSION"
},
"ng-update": {
"packageGroup": "NG_UPDATE_PACKAGE_GROUP"
},
"repository": {
"type": "git",
"url": "https://github.com/angular/universal"
},
"bugs": {
"url": "https://github.com/angular/universal/issues"
},
"homepage": "https://github.com/angular/universal"
}
8 changes: 8 additions & 0 deletions modules/socket-engine/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export { startSocketEngine, SocketEngineResponse, SocketEngineRenderOptions } from './src/main';
91 changes: 91 additions & 0 deletions modules/socket-engine/spec/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@

import { ServerModule } from '@angular/platform-server';
import { NgModule, Component } from '@angular/core';
import 'zone.js';

import { BrowserModule } from '@angular/platform-browser';
import { startSocketEngine, SocketEngineResponse,
SocketEngineRenderOptions } from '@nguniversal/socket-engine';
import * as net from 'net';

export function makeTestingModule(template: string, component?: any): any {
@Component({
selector: 'root',
template: template
})
class MockComponent {}
@NgModule({
imports: [ServerModule, BrowserModule.withServerTransition({appId: 'mock'})],
declarations: [component || MockComponent],
bootstrap: [component || MockComponent]
})
class MockServerModule {}
return MockServerModule;
}

async function sendAndRecieve(renderOptions: SocketEngineRenderOptions, template = '') {
return new Promise<SocketEngineResponse>(async(resolve, _reject) => {

const appModule = makeTestingModule(template);
const server = await startSocketEngine(appModule);

const client = net.createConnection(9090, 'localhost', () => {
client.write(JSON.stringify(renderOptions));
});

client.on('data', data => {
const res = JSON.parse(data.toString()) as SocketEngineResponse;
server.close();
resolve(res);
});
});
}

describe('test runner', () => {
it('should render a basic template', async (done) => {
const template = `${new Date()}`;
const renderOptions = {id: 1, url: '/path',
document: '<root></root>'} as SocketEngineRenderOptions;
const result = await sendAndRecieve(renderOptions, template);
expect(result.html).toContain(template);
done();
});
it('should return the same id', async(done) => {
const id = Math.random();
const renderOptions = {id , url: '/path',
document: '<root></root>'} as SocketEngineRenderOptions;
const result = await sendAndRecieve(renderOptions);
expect(result.id).toEqual(id);
done();
});
it('should return an error if it cant render', async(done) => {
@Component({
selector: 'root',
template: ''
})
class MockComponent {constructor(_illMakeItThrow: '') {}}
const appModule = makeTestingModule('', MockComponent);
const server = await startSocketEngine(appModule);

const client = net.createConnection(9090, 'localhost', () => {
const renderOptions = {id: 1, url: '/path',
document: '<root></root>'} as SocketEngineRenderOptions;
client.write(JSON.stringify(renderOptions));
});

client.on('data', data => {
const res = JSON.parse(data.toString()) as SocketEngineResponse;
server.close();
expect(res.error).not.toBeNull();
done();
});
});
it('should return an error if it cant render', async(done) => {
const template = `${new Date()}`;
const renderOptions = {id: 1, url: '/path',
document: '<root></root>'} as SocketEngineRenderOptions;
const result = await sendAndRecieve(renderOptions, template);
expect(result.error).toBeUndefined();
done();
});
});
55 changes: 55 additions & 0 deletions modules/socket-engine/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { ɵCommonEngine as CommonEngine,
ɵRenderOptions as RenderOptions } from '@nguniversal/common/engine';
import { NgModuleFactory, Type } from '@angular/core';
import * as net from 'net';

export interface SocketEngineServer {
close: () => void;
}

export interface SocketEngineRenderOptions extends RenderOptions {
id: number;
}

export interface SocketEngineResponse {
id: number;
html: string|null;
error?: Error;
}

export function startSocketEngine(
moduleOrFactory: Type<{}> | NgModuleFactory<{}>,
host = 'localhost',
port = 9090
): Promise<SocketEngineServer> {
return new Promise((resolve, _reject) => {
const engine = new CommonEngine(moduleOrFactory);

const server = net.createServer(socket => {
socket.on('data', async buff => {
const message = buff.toString();
const renderOptions = JSON.parse(message) as SocketEngineRenderOptions;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi all

I've been playing around with the socket-engine a bit and noticed an in my opinion significant error. My setup: Connect to the socket from an Apache Tomcat container. I am honestly new to sockets and followed this guide, ended up something like this.

WebSocketClient simpleWebSocketClient = new StandardWebSocketClient();
List<Transport> transports = new ArrayList<>(1);
transports.add(new WebSocketTransport(simpleWebSocketClient));
SockJsClient sockJsClient = new SockJsClient(transports);

WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());

String url = "ws://localhost:9090/";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
StompSession session = stompClient.connect(url, sessionHandler).get();

Now the problem is in the implementation of the Apache org.apache.tomcat.websocket.WsWebSocketContainer.java which sends some default parameters on the initial request. You can see the connect statement and it's unskippable parameters in this file on lines 266 to 275.

So here we are now, sending an initial request to the websocket. An informational request which is not in JSON format. This line tries to parse it - and dos not return an error -> The connection attempt ends in a timeout and the webapplication failes to start!

As far as I see you should cover this line by a try-catch too and handle errors in a maner way?

I'd like to state out once more that this is just what I noticed in a brief test. Also I'm not an Apache pro, just looked into the source code and didn't see a way to skip the initial request (except writing my own implementation, which is maybe a bit too overkill). Just wanted to let you know what I detected. It may be intentional that this line isn't covered by a try-catch, it may not. Up to you.

Cheers

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That inital request is actually setting up the WS conenction.

// Create the initial HTTP request to open the WebSocket connection

A WebSocket connection starts out as a normal Http connection and once it figures out that both the client and server support WS it's Upgraded to a WS connection, this is what that inital connection is doing.

HOWEVER; We are not using WebSockets here.

We are using raw TCP Sockets, meaning, no http, no WS, only a raw TCP Socket.
I don't know what it's called in Java but it isn't WebSockets

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's the thing. Sorry and thank you for the explanation.

try {
const html = await engine.render(renderOptions);
socket.write(JSON.stringify({html, id: renderOptions.id} as SocketEngineResponse));
} catch (error) {
// send the error down to the client then rethrow it
socket.write(JSON.stringify({html: null,
id: renderOptions.id, error} as SocketEngineResponse));
throw error;
}
});
});

server.listen(port, host);
resolve({close: () => server.close()});
});
}

Loading