Skip to content
This repository has been archived by the owner on Mar 8, 2022. It is now read-only.

Typescript retozi #21

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
111 changes: 111 additions & 0 deletions submissions/retozi/Actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {Action, RequestAction} from './Flux';
import {AllState} from './state/AllState';
import * as SithLordsState from './state/SithLordsState';
import * as ObiWanWorldState from './state/ObiWanWorldState';
import * as Flux from './Flux';

function makePendingSithLord(res: Flux.Request): SithLordsState.SithLord {
const template = SithLordsState.emptySithLord();
template.status = SithLordsState.SithLordStatus.PENDING;
template.request = res;
return template;
}

function makeSithLord(res: Flux.Request): SithLordsState.SithLord {
var body: any = res.body;

return {
apprenticeUrl: (body.apprentice) ? body.apprentice.url : null,
masterUrl: (body.master) ? body.master.url : null,
name: body.name,
id: body.id,
homeworldName: body.homeworld.name,
homeworldId: body.homeworld.id,
status: SithLordsState.SithLordStatus.PRESENT,
request: null
};
}

function parseSithLordFromRequest(res: Flux.Request): SithLordsState.SithLord {
return (res.pending) ? makePendingSithLord(res) : makeSithLord(res);
}


function onReceiveSithOrReceiveObiWanWorld(state: AllState) {
for (const l of state.sithLords.v) {
if (l.homeworldId === state.obiWanWorld.v.id) {
;
}
}
}

export class GET_SIDIOUS extends RequestAction {
constructor() {
super('http://localhost:3000/dark-jedis/3616');
}

write(state: AllState) {
const sidious = parseSithLordFromRequest(this.req);
state.sithLords = SithLordsState.writeSidious(state.sithLords, sidious);
state.obiWanWorld = ObiWanWorldState.writeSithPresent(state.obiWanWorld, state.sithLords.v);
}
}


export class GET_SITH_APPRENTICE extends RequestAction {
masterId: number;

constructor(url: string, masterId: number) {
this.masterId = masterId;
super(url);
}

write(state: AllState) {
const lord = parseSithLordFromRequest(this.req);
state.sithLords = SithLordsState.writeApprentice(state.sithLords, lord, this.masterId);
state.obiWanWorld = ObiWanWorldState.writeSithPresent(state.obiWanWorld, state.sithLords.v);
}
}

export class GET_SITH_MASTER extends RequestAction {
apprenticeId: number;

constructor(url: string, apprenticeId: number) {
this.apprenticeId = apprenticeId;
super(url);
}

write(state: AllState) {
const lord = parseSithLordFromRequest(this.req);
state.sithLords = SithLordsState.writeMaster(state.sithLords, lord, this.apprenticeId);
state.obiWanWorld = ObiWanWorldState.writeSithPresent(state.obiWanWorld, state.sithLords.v);
}
}

export class RECEIVE_OBI_WORLD extends Action {
worldId: number;
worldName: string;

constructor(id: number, name: string) {
this.worldId = id;
this.worldName = name;
super();
}

write(state: AllState) {
state.obiWanWorld = ObiWanWorldState.writeWorld(this.worldId, this.worldName, state.sithLords.v);
}
}

export class SCROLL_UP extends Action {
write(state: AllState) {
state.sithLords = SithLordsState.writeScrollUp(state.sithLords);
}
}


export class SCROLL_DOWN extends Action {
write(state: AllState) {
state.sithLords = SithLordsState.writeScrollDown(state.sithLords);
}
}
181 changes: 181 additions & 0 deletions submissions/retozi/Flux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import * as React from 'react';
import {AllState} from './state/AllState';
var shallowCompare = require('react/lib/shallowCompare');


interface ChangeCallback {
(): void;
}

// the single state atom wrapper
export class Store {
state: AllState;
private callbacks: ChangeCallback[];

constructor(initialState: AllState) {
this.callbacks = [];
this.state = initialState;
}
addChangeListener(callback: ChangeCallback): void {
this.callbacks.push(callback);
}

removeChangeListener(callback: ChangeCallback): void {
var index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
}

writeAction(action: Action): void {
action.write(this.state, new ActionCreator(this));
this.callbacks.forEach((cb: ChangeCallback) => cb());
}
}

// a synchronous action
export abstract class Action {
// an action must implement a write method that changes the state atom
abstract write(state: AllState, actionCreator?: ActionCreator): void
name(): string {
return this.constructor.toString().match(/function (\w*)/)[1];
}
}

// a small wrapper arround XMLHttpRequest to remove complexity
export class Request {
private req: XMLHttpRequest;
private url: string;

constructor(url: string) {
this.url = url;
this.req = new XMLHttpRequest();
}

get pending(): boolean {
return this.req.readyState < 4;
}

get returnedSucessfully(): boolean {
return this.req.readyState === 4 && this.req.status === 200;
}

send(cb: () => void): void {
this.req.onreadystatechange = () => {
if (this.returnedSucessfully) {
cb();
}
};
this.req.open("GET", this.url);
this.req.send();
}

get body(): any {
if (this.returnedSucessfully) {
return JSON.parse(this.req.response);
}
}

abort(): void {
this.req.abort();
}
}

// An action that gets sent to the writer twice. once when request sent, once
// when request returns
export abstract class RequestAction extends Action {
req: Request;

constructor(url: string) {
super();
this.req = new Request(url);
}
/*
a request Action additionally implements a send method
so that the action can be dispatched twice. Once synchronously
("pending"), once asynchrounsly when returning

this is the only place where asynchronicity happens!
*/
send(writer: (a: Action) => void): void {
writer(this);
this.req.send(() => writer(this));
}
}


export class ActionCreator {
private store: Store;
constructor(store: Store) {
this.store = store;
}
write(action: Action): void {
this.store.writeAction(action);
}

send(requestAction: RequestAction): void {
requestAction.send(this.write.bind(this));
}
}

interface ContainerProps {
store: Store;
}

export abstract class Container<S> extends React.Component<ContainerProps, S> {
private onAction: () => void;
abstract stateSelector(state: AllState, actionCreator?: ActionCreator): S

constructor(props: ContainerProps) {
super(props);
this.state = this.getStoreState();

// bound method
this.onAction = (): void => {
this.setState(this.getStoreState());
};
}

private getStoreState(): S {
return this.stateSelector(this.props.store.state, new ActionCreator(this.props.store));
}


shouldComponentUpdate(nextProps: ContainerProps, nextState: S): boolean {
return shallowCompare(this, nextProps, nextState);
}

componentDidMount(): void {
this.props.store.addChangeListener(this.onAction);
}

componentWillUnmount(): void {
this.props.store.removeChangeListener(this.onAction);
}

}

// a typed immutable wrapper for any object. Returns a copy if
// the value getter is called.
export class Immutable<S> {
private obj: S;

constructor(obj: S) {
this.obj = obj;
}

get v(): S {
if (Array.isArray(this.obj)) {
return (<any> this.obj).slice();
} else if (this.obj && typeof this.obj === 'object') {
// to lazy to define typing for Object.assign
return (<any> Object).assign({}, this.obj);
} else {
return this.obj;
}
}

set(f: (obj: S) => S): Immutable<S> {
return new Immutable(f(this.v));
}
}
54 changes: 54 additions & 0 deletions submissions/retozi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Flux Challenge

This is the demonstration of the current architecture we use at my workplace to build a production web app.

Some notes on the design decisions / rules we enforce:

### Use of Typescript

Everything must be strictly typed (even immutable data structures). Reasoning here: https://www.youtube.com/watch?v=-WKGKofd2DI


### Flux

The example comes with an own """Flux""" implementation. (I believe it's not worth to use an external dependency. A decent, modern implementation
is roughly 200-300 straightforward LOC and should be tailored to the needs of a project.)

It is comparable to redux:

* a single state atom
* immutability to track state changes
* a functional way to update the state


However, there are a couple of differences.

* Actions are implemented as classes. The action implements the reducer function as its own method. The reasoning here: http://www.code-experience.com/problems-with-flux/ .
* The async flow is strictly isolated inside the RequestAction and translated into two, synchronous state updates.
* There is only one place where queries for state from the server are triggered: Inside the selector of a state slice.
* A typed solution for immutability.


##### Async flow

Async code is evil. It is notoriously hard to reason about, and if you are not careful, it leaks all over your flux architecture, making things very complex.

There are no promises or other async helpers in this example. All async behavior is wrapped inside the RequestAction class and does not leak outside.

If you need to talk to the server, you must dispatch a RequestAction.

The Store will call the writer of this action twice: Once when the request is sent (i.e. pending), once when the request returns.
Both the "request sent" write and "response received" write are being multiplexed into the write method of the RequestAction.



#### Bootstrapping and updating server state

I've never seen a big discussion about this in the flux community, but it is somewhat of a hard problem: Where do you trigger your Actions that query state from the server?

Most fetches are triggered by a user interaction with the DOM. But some need to be triggered when bootstrapping the "initial state" from the server.

This is solved by only triggering query Actions in the selector function of a given state slice. So fetches will be automatically triggered when a container pulls in a
certain state slice. The selector function is declared in the state slice module (and not in the Container) for DRY purposes.

To "refresh" state from the server, simply fire a regular sync action that puts a state slice in a state where the selector triggers the RequestAction side-effect.
Loading