Skip to content

Commit

Permalink
Merge pull request #4 from OKNoah/mapStateToProps
Browse files Browse the repository at this point in the history
Adds mapStateToProps
  • Loading branch information
OKNoah authored Dec 31, 2017
2 parents fe2d2d8 + a75ad58 commit 73527be
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 160 deletions.
54 changes: 42 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
> ⚠️ NOTE: Future releases will be pre-release and have `alpha` and `beta` tags. NPM/yarn will probably not install these unless you specify.
# Final

This is a very experimental proof of concept for a sort of MC framework. It's meant to work a little like React, but for handling web requests. One of the posibilities would be very precise handling of how to respond to requests, possibly allowing redux store-like functionality.
This is a very experimental proof of concept for a server framework. It takes ideas from ES6+ and React to see if there's a more enjoyable, versatile way to create APIs. Key components are:

#### Classes

Each endpoint has a class, which extends the component class. The main method of the class is `respond` which decides what is returned upon each request.

#### Lifecycle

Each component has a lifecycle, with names similar to the lifecycle components of React components.

#### Decorators

Decorators are used to add actions and in-memory state to components. The example decorator that's included is for ArangoDB. A `reduxConnect` decorator is also currently part of the library.

#### WebSockets

WebSocket-functionality is built-in (at present). Depending on how you wish to use the library, you can have components that repeat their lifecycle whenever state is changed. This is most useful for WebSocket servers becuase there may be multiple responses per connection. An `http` request will fire `response` onece, but a WebSocket connection might do it many times.

## Usage

See the [`/examples`](examples) and files matching the patter [`**/*.test.js`](src) for more usage and explanation. Be warned the API will change a lot.

```js
import Final from './src/index'
import Final, { reduxConnect } from './src/index'
import { bindActionCreators } from 'redux'
import { findDecorator } from './test/ArangoDecorator'
import { middleware, store } from './example/middleware'
import { moveUp } from './redux/modules/player'

/*
The `findDecorator` adds a few funtions to the class, like `this.findOne`.
Expand All @@ -16,29 +38,37 @@ import { middleware, store } from './example/middleware'
// this decorator will verify collection or create new one
collection: 'Post'
})
@reduxConnect(
(state) => ({
players: state
}),
(dispatch) => ({
moveUp
})
)
class Post extends Final.Component {
/*
The path decides what requests will match this component and the params.
*/
path = '/post/:post?'
constructor () {
super()
}

/*
The respond function returns whatever the response will be. Notice the params and `this.findOne` are available.
*/
async respond () {
console.log('this.props.params', this.props.params)
const output = await this.findOne({"body": "Updated!"})
return output
console.log('this.actions.moveUp', this.action.moveUp)
const output = await this.actions.findOne({"body": "Updated!"})
return {
data: {
players: this.props.players,
output
}
}
}
}

Final.createServer({
components: [Post],
port: 3001,
middleware, // optional, see `examples/middleware-usage.js`
store // optional, see `examples/middleware-usage.js`
store, // optional, see `examples/game-server.js`
// middleware, /* Removed. Purpose needs to be decided. */
})
```
66 changes: 17 additions & 49 deletions examples/game-server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createStore, bind, bindActionCreators } from 'redux'
import { createStore, bindActionCreators, combineReducers } from 'redux'
import { createServer, Component, reduxConnect } from '../src/index'
import { makeBug } from '../test/game-server_fake-player.js'

const PORT = 3001

Expand Down Expand Up @@ -65,62 +66,34 @@ const reducer = (state = {}, action) => {
}
}

const globalStore = createStore(reducer)

/*
This is just some code to allow adding fake users for testing (and fun). Perhaps it useful to the example too.
*/
const bugs = []

class Bug {
constructor (player, interval = 2000) {
this.player = player
this.timeout = interval
}

move () {
const type = ['map/MOVE_UP', 'map/MOVE_DOWN', 'map/MOVE_RIGHT', 'map/MOVE_LEFT'][Math.floor(Math.random() * 200) % 4]

globalStore.dispatch({ type, player: this.player })
}

run () {
setInterval(() => {
this.move()
}, 2000)
}
}

function makeBug () {
function getRandomColor () {
const letters = '0123456789ABCDEF'
let color = ''
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)]
}
return color
}

const bug = new Bug(getRandomColor())
const reducers = combineReducers({
players: reducer
})

bugs.push(bug.run())
}
const globalStore = createStore(reducers)

const moveUp = (player) => ({ type: 'map/MOVE_UP', player })
const moveDown = (player) => ({ type: 'map/MOVE_DOWN', player })
const moveLeft = (player) => ({ type: 'map/MOVE_LEFT', player })
const moveRight = (player) => ({ type: 'map/MOVE_RIGHT', player })
const init = (player) => ({ type: 'map/INIT', player })

/*
This is a "Bug", a fake user/player that moves around randomly. Not to be confused with a software glitch/bug. TODO: Probably change name of this to avoid confusion. See `game-server_fake-player.js`.
*/
const bug = makeBug(globalStore)

@reduxConnect(
null,
(state) => ({
players: state.players
}),
(dispatch) => bindActionCreators({
moveUp,
moveDown,
moveLeft,
moveRight,
init,
bug: makeBug
bug
}, dispatch)
)
class User extends Component {
Expand All @@ -129,14 +102,9 @@ class User extends Component {
super()
}

async responseWillOccur () {
const { player } = this.props.params

this.actions.init(player)
}

async messageReceived (msg) {
if (['moveUp', 'moveDown', 'moveLeft', 'moveRight', 'bug'].includes(msg)) {
console.log('this.actions', this.actions)
this.actions[msg](this.props.params.player)
} else {
throw "That's not a function"
Expand All @@ -147,7 +115,7 @@ class User extends Component {

async respond () {
return {
data: this.store.getState()
data: this.props.players
}
}
}
Expand Down
3 changes: 0 additions & 3 deletions examples/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import { findDecorator } from '../test/ArangoDecorator'
})
class User extends Final.Component {
path = '/user/:user?'
constructor () {
super()
}

async respond () {
await this.findOne({ "body": "Updated!" })
Expand Down
28 changes: 23 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
{
"name": "final",
"version": "1.0.0-alpha.1",
"main": "index.js",
"author": "Noah",
"name": "final-server",
"description": "A component-based server framework for ES6+, with a React-like lifecycle. Experimental.",
"version": "0.9.1",
"main": "dist/index.js",
"author": "Noah Gray <[email protected]> (https://github.com/oknoah)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/OKNoah/final.git"
},
"scripts": {
"test:path": "./node_modules/.bin/tape -r @babel/register",
"prepublish": "./node_modules/.bin/babel ./src -d ./dist --ignore node_modules/ --ignore dist/",
"test": "./node_modules/.bin/tape -r @babel/register './src/**/*.test.js' | ./node_modules/.bin/tap-notify | ./node_modules/.bin/faucet",
"test:path": "./node_modules/.bin/tape -r @babel/register",
"test:watch": "./node_modules/.bin/onchange '**/*.js' -- yarn run test",
"test:pathwatch": "./node_modules/.bin/onchange '**/*.js' -- yarn run test:path",
"test:game-server": "curl -s -X GET --header 'Accept: application/json' 'http://localhost:3001/map/1/player/1234' | node -r @babel/register -e \"process.stdin.setEncoding('utf8'); var chunks = ''; process.stdin.on('readable', () => { const chunk = process.stdin.read(); if (chunk !== null) { chunks += chunk }}); process.stdin.on('end', () => { process.stdout.write(JSON.stringify(JSON.parse(chunks), null, 2)) ;});\""
},
"keywords": [
"rest",
"restful",
"api",
"websockets",
"sockets",
"framework",
"server",
"arango",
"orm",
"web"
],
"devDependencies": {
"@babel/cli": "^7.0.0-beta.35",
"@babel/core": "^7.0.0-beta.35",
Expand Down
58 changes: 33 additions & 25 deletions src/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ export default class Final {
path = '/'
response = {};
state = {};
props = {};
lifecycle = [
this.requestWillBeReceived.bind(this),
this.responseWillOccur.bind(this),
this.componentWillReceiveProps.bind(this),
this.shouldComponentUpdate.bind(this),
this.componentWillRespond.bind(this),
this.respond.bind(this),
this.responseDidEnd.bind(this)
];

constructor () {
this.props = {}
}

static async setState (instance, state) {
instance.state = state

Expand All @@ -31,49 +35,53 @@ export default class Final {
return
}

async requestWillBeReceived (req) {
this.request = req
const match = route(this.path)
const params = match(parse(this.request.url).pathname)
this.props.params = params
if (!this.props.params) {
throw 'No match'
}
static async setProps (instance, state) {
instance.props = state

return
}

async messageReceived () {
async setProps (state) {
await this.constructor.setProps(this, state)

return
}

async requestReceived () {
async shouldComponentUpdate (newProps) {
if (newProps !== this.props) {
return true
}

return false
}

async componentWillReceiveProps () {
return
}

async responseWillOccur () {
async messageReceived () {
return
}

async responseWillEnd () {
async componentWillRespond () {
return
}

async responseDidEnd (res) {
async responseDidEnd () {
const data = JSON.stringify(this.state)
const length = Buffer.byteLength(JSON.stringify(data))
const length = Buffer.byteLength(data)

try {
res.writeHead(200, {
this.props.response.writeHead(200, {
'Content-Length': length,
'Content-Type': 'application/json'
})
res.end(data)
} catch (e) {
try {
res.send(data)
} catch (e) {
console.error(e)
this.props.response.end(data)
if (this.end) {
this.end()
}
} catch (e) {
this.props.response.send(data)
}
return
}
Expand All @@ -88,6 +96,6 @@ export default class Final {
}

async tick () {
this.lifecycleIncrement++
return this.lifecycleIncrement + 1
}
}
9 changes: 2 additions & 7 deletions src/Component.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import test from 'tape'
import client from 'superagent'
import Final, { reduxConnect } from './index'
import { middleware, store } from '../examples/middleware'
import { store } from '../examples/middleware'
import { findDecorator } from '../test/ArangoDecorator'
import { createStore, bind, bindActionCreators } from 'redux'
import { bindActionCreators } from 'redux'
import WS from 'ws'

const HOST = 'localhost:3001'

function testo () { return { type: 'TEST' } }

@reduxConnect(
null,
(dispatch) => bindActionCreators({
Expand All @@ -21,9 +19,6 @@ function testo () { return { type: 'TEST' } }
})
class User extends Final.Component {
path = '/user/:user?'
constructor (props) {
super(props)
}

async respond () {
await this.save({ "body": "Updated!" })
Expand Down
Loading

0 comments on commit 73527be

Please sign in to comment.