-
Notifications
You must be signed in to change notification settings - Fork 483
feat(socket-engine): introduce package #999
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"], | ||
) |
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)); | ||
``` |
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'; |
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" | ||
} |
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'; |
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(); | ||
}); | ||
}); |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Now the problem is in the implementation of the Apache 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That inital request is actually setting up the WS conenction.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()}); | ||
}); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolve this
There was a problem hiding this comment.
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