Repository: mozilla/reflex Branch: master Commit: c5e75e98bc60 Files: 20 Total size: 40.3 KB Directory structure: gitextract_sq4vfbil/ ├── .babelrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── History.md ├── License.md ├── Readme.md ├── package.json ├── src/ │ ├── application.js │ ├── dom.js │ ├── effects.js │ ├── html.js │ ├── preemptive-animation-frame.js │ ├── reflex.js │ ├── signal.js │ ├── subscription.js │ └── task.js └── test/ └── test-api.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "sourceMaps": "inline", "comments": false, "presets": [ "flow-node" ] } ================================================ FILE: .flowconfig ================================================ [ignore] .*/dist/.* .*/node_modules/babel.* .*/node_modules/tap/* .*/node_modules/json5/test/* .*/node_modules/kefir/* .*/node_modules/documentation/* [libs] [include] [options] suppress_comment= \\(.\\|\n\\)*\\@FlowIssue suppress_comment= \\(.\\|\n\\)*\\@FlowIgnore experimental.strict_type_args=true ================================================ FILE: .gitignore ================================================ lib dist **/*.js **/*.flow !src/**/*.js !src/**/*.flow !test/**/*.js !test/**/*.flow ================================================ FILE: .npmignore ================================================ *~ ~* !dist !lib !**/*.js !**/*.flow ================================================ FILE: .travis.yml ================================================ language: node_js sudo: false node_js: - stable deploy: provider: npm email: rfobic@gmail.com on: tags: true api_key: secure: 0SAU8CJgSNo9jMZub3DRPPpqNnYPoNPUY6lnEunPHDLxwCLOwv7s6fBrSsCOvGifp7OgqMu+gBcZQcmNf5jsLqFnm3y0xEqxkabZs2x6M7FPaY3fEr7G5jDcfgO+cqJtBFsEFgRKGoZt6yhXB8Cy8/Tr/uroyAzHLifWVQURoumfy5GelosB9Tjy1pCThGwoz+20zHUk5amqdgcJiauwsSeDRS3fpOFUn7rLlpcfP44+IzF8szOdNY5wkaL2LDgCOOcK+J5k7X0iHD7E/T7mCzekbg7HCU9Cj+3nou7QsWn/NLxhfVPwa+OCHFdPL88V+kG5hzHj92NMloezafb/jBTG2UaL9QEBoqwA/mRqjcwywT6ekFlt/E2WqaYrQcKBcDRgrp5L7L99E+OczTao0PWKPnBz9xL0/q6mbf560voLArCQznduzmt0ELV4d5rYrtUGxH/Rkt9sO+Oj62mII9ODOcqcEuxH0/8MU3NWh84aajBzBDVjOobNPPv2KmUOfe1BxPUDPw6AR+aphDfEqItHQ3zVsW3Gsx+oetdfDEYIwQhCEPWDZ/hHkRps63UxqoA7r2iRqyUaPT1yvM8KVigvRq5dph8a4w47m8Fz9ckZp/j1l9e53QXAB/EYw/ziqxc9ihc1TJCBKGY61rMWeRzoKehFd4pgjsTJIQIIeU4= ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Community Participation Guidelines This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. ================================================ FILE: History.md ================================================ # Changes ## 0.0.3 / 2012-12-02 - Implement [model](./model) abstraction that can be used to map data to a reactors that handle IO for the specific components in the application. - Implement [collection](./collection) abstraction that is similar to model but handles collections of things. - Added some examples. - Tests. ## 0.0.2 / 2012-10-23 - Implement notion of [state](./state.js) that can be `diff`-ed & `patch`-ed. - Implement [writer](./writer.js) high order function for making `write`-ers that can reflect state changes on the output. ## 0.0.1 / 2012-10-20 - Initial draft ================================================ FILE: License.md ================================================ Copyright 2012 Irakli Gozalishvili. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Readme.md ================================================ # reflex [![NPM version][version.icon]][version.url] [![Build Status][travis.icon]][travis.url] [![Gitter][gitter.icon]][gitter.url] [![styled with prettier][prettier.icon]][prettier.url] Reflex is a functional reactive UI library that is heavily inspired by (pretty much is a port of) [elm][] and it's amazingly simple yet powerful [architecture][elm architecture] where "[flux][]" in [react][] terms is simply a byproduct of a pattern. In order to keep a major attraction of [elm][] — [algebraic data types][] & type safety — the library uses [flow][], a static type checker for JS. All types are separated from implementation though, so it's your call if you want to take advantage of it or just ignore it. The library is designed such that view drivers ([react][react-driver], [virtual-dom][virtual-dom-driver] & possibly more in the future) can be swapped without any changes to the application code base. In fact there is not a built-in view driver, so it's up to the user to choose one. In fact it's pretty easy to write a driver that would directly manipulate DOM. ## Install npm install reflex ## Examples For examples check out examples directory of either [virtual-dom][virtual-dom-driver] or [react][react-driver] drivers, in fact examples are identical only diff is one line which is path of imported driver. [elm]:http://elm-lang.org [elm architecture]:http://elm-lang.org/guide/architecture [react]:http://facebook.github.io/react/ [immutable.js]:https://facebook.github.io/immutable-js/ [flux]:https://facebook.github.io/flux/ [algebraic data types]:https://en.wikipedia.org/wiki/Algebraic_data_type [flow]:http://flowtype.org [virtual-dom-driver]:https://github.com/mozilla/reflex-virtual-dom-driver [react-driver]:https://github.com/mozilla/reflex-react-driver [version.url]: https://npmjs.org/package/reflex [version.icon]: https://img.shields.io/npm/v/reflex.svg?style=flat [travis.url]: https://travis-ci.org/mozilla/reflex [travis.icon]: https://img.shields.io/travis/mozilla/reflex.svg?style=flat [gitter.url]: https://gitter.im/mozilla/reflex?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge [gitter.icon]: https://badges.gitter.im/Join%20Chat.svg [prettier.url]:https://github.com/prettier/prettier [prettier.icon]:https://img.shields.io/badge/styled_with-prettier-ff69b4.svg ================================================ FILE: package.json ================================================ { "name": "reflex", "version": "1.1.0", "description": "Functional reactive UI library", "keywords": ["reflex", "reactive", "functional", "UI"], "author": "Irakli Gozalishvili (http://jeditoolkit.com)", "main": "reflex", "devDependencies": { "babel-cli": "6.24.1", "babel-preset-flow-node": "^1.0.2", "babel-register": "6.24.1", "blue-tape": "^1.0.0", "documentation": "^4.0.0-rc.1", "flow-bin": "^0.49.1", "flow-copy-source": "^1.2.0", "husky": "^0.14.0", "lint-staged": "^4.0.0", "prettier": "^1.4.4" }, "scripts": { "test": "npm run test:flow && npm run test:tape", "test:tape": "blue-tape -r babel-register test/**/*.js", "test:flow": "flow check", "build:clear": "rm -rf ./*.js && rm -rf ./*.js.flow && rm -rf reflex", "build:types": "flow-copy-source -v src .", "build:node": "babel --out-dir . src", "build:api": "documentation readme --section=API src/reflex.js", "build:docs": "documentation build --document-exported src/** -f html --o docs", "build": "npm run build:node && npm run build:types", "prepublish": "npm run build && npm test", "precommit": "lint-staged" }, "lint-staged": { "*.js": ["prettier --parser flow --no-semi --write", "git add"] }, "repository": "https://github.com/mozilla/reflex", "license": "MIT", "dependencies": { "reflex-driver": "^1.0.2" } } ================================================ FILE: src/application.js ================================================ /* @flow */ import { Task } from "./task" import { Effects } from "./effects" import { LazyRoot } from "./dom" import { Node } from "reflex-driver" import type { Address } from "./signal" import { Subscription, Feed, unsubscribe } from "./subscription" import type { Service, Subscribe, Subscriber } from "./subscription" export type Init = ( flags: flags ) => [model, Effects] export type Update = ( state: model, action: action ) => [model, Effects] export type View = ( state: model, address: Address ) => Node export type Subscriptions = ( state: model ) => Subscription type Services = { nextAddress: number, outbox: Address, active: { [key: string]: Service } } class Application { constructor( send: Address, model: state, view: Node, task: Task, services: Services ) { this.send = send this.model = model this.view = view this.task = task this.services = services } send: Address model: state view: Node task: Task services: Services } export type { Application } export type Driver = ( state: Application ) => void export type BeginnerConfiguration = { model: model, update: (model: model, action: action) => model, view: View } export type AdvancedConfiguration = { flags: flags, init: Init, update: Update, view: View, subscriptions?: Subscriptions } const first = (xs: [a, b]): a => xs[0] const second = (xs: [a, b]): b => xs[1] export const beginner = ( configuration: BeginnerConfiguration ): AdvancedConfiguration => ({ flags: void 0, init: _ => [configuration.model, Effects.none], update: (model, action) => [ configuration.update(model, action), Effects.none ], view: configuration.view, subscriptions: unsubscribe }) export const start = ( configuration: AdvancedConfiguration, drive: Driver ): Application => { const { init, view, update, flags } = configuration const subscriptions: Subscriptions = configuration.subscriptions == null ? unsubscribe : configuration.subscriptions const send = action => { const [model, fx] = update(application.model, action) application.model = model application.view = new LazyRoot(view, model, send) application.task = fx.execute(send) application.services = subscriptions(model).reduce( subscribe, application.services ) exectueServices(application.services, send) drive(application) } const [state, fx] = init(flags) const application = new Application( send, state, new LazyRoot(view, state, send), fx.execute(send), subscriptions(state).reduce(subscribe, { nextAddress: 0, outbox: send, active: Object.create(null) }) ) exectueServices(application.services, send) drive(application) return application } const subscribe = ( services: Services, subscription: Subscribe ): Services => { const { active, outbox } = services const { feed, detail, tagger } = subscription const service = active[String(feed.address)] if (service == null || service.feed !== feed) { const address = `/${++services.nextAddress}` //` active[address] = spawnService(address, subscription, feed, outbox) } else { service.subscribers.push(subscription) } return services } const spawnService = ( address: string, subscription: Subscriber, feed: Feed, outbox: Address ): Service => { const subscribers = [subscription] const state = feed.init() feed.address = address const send = (input: inn) => { const [state, fx] = feed.update(service.state, input, outbox) service.state = state fx.execute(send) } const service = { feed, subscribers, state, inbox: send } return service } const exectueServices = (services: Services, send: Address) => { for (let address in services.active) { exectueService(services.active[address], send) } } const exectueService = ( service: Service, outbox: Address ) => { const { state, feed, subscribers, inbox } = service const [next, fx] = feed.update(state, feed.subscribe(subscribers), outbox) service.state = next fx.execute(inbox) } ================================================ FILE: src/dom.js ================================================ /* @flow */ import { Driver, Node } from "reflex-driver" import type { Properties } from "reflex-driver" import type { Address } from "./signal" export class LazyRoot implements Node { state: model view: (state: model, mailbox: Address) => * mailbox: Address constructor( view: (state: model, mailbox: Address) => *, state: model, mailbox: Address ) { this.state = state this.view = view this.mailbox = mailbox } renderWith(renderer: Driver): node { driver = renderer return this.view(this.state, this.mailbox) } } class ErrorDriver implements Driver { createElement(..._): Node { throw new Error(`You need to use a reflex driver to create element nodes`) } createElementNS(..._): Node { throw new Error(`You need to use a reflex driver to create element nodes`) } createTextNode(..._): Node { throw new Error(`You need to use a reflex driver to create text nodes`) } createThunk(..._): Node { throw new Error(`You need to use a reflex driver to create thunk nodes`) } render(node: Node): void { throw new Error(`You need to use a reflex driver to render nodes`) } } let driver: Driver = new ErrorDriver() export const text = (content: string): Node => driver.createTextNode(content) export const element = ( tagName: string, properties: ?Properties, children: ?Array ): Node => driver.createElement(tagName, properties, children) export const elementNS = ( namespaceURI: string, tagName: string, properties: ?Properties, children: ?Array ): Node => driver.createElementNS(namespaceURI, tagName, properties, children) export const thunk: ( key: string, view: ( a0: a, a1: b, a2: c, a3: d, a4: e, a5: f, a6: g, a7: h, a8: i, a9: j ) => Node, a0: a, a1: b, a2: c, a3: d, a4: e, a5: f, a6: g, a7: h, a8: i, a9: j ) => Node = (key, view, ...args) => driver.createThunk(key, view, (args: any)) ================================================ FILE: src/effects.js ================================================ /* @flow */ import { Task } from "./task" import type { Address } from "./signal" export type Time = number const raise = error => { throw Error( `Effects should be created from task that empty fail but it did fail with error ${error}` ) } const ignore = _ => void 0 const nil = Task.succeed(void 0) const empty = new Task((succeed, fail) => void 0) export class Effects { static task(task: Task): Effects { console.warn( "Effects.task is deprecated please use Effects.perform instead" ) return new Perform(task) } static perform(task: Task): Effects { return new Perform(task) } static tick(tag: (time: number) => a): Effects { console.warn( "Effects.tick is deprecated please use Effects.perform(Task.requestAnimationFrame().map(tag)) instead" ) return new Perform(Task.requestAnimationFrame().map(tag)) } static receive(action: a): Effects { const fx = new Perform( new Task( (succeed, fail) => void Promise.resolve(action).then(succeed, fail) ) ) return fx } static batch(effects: Array>): Effects { return new Batch(effects) } map(f: (a: a) => b): Effects { throw Error("Subclass of abstract Effect must implement map") } execute(address: Address): Task { throw Error("Subclass of abstract Effect must implement execute") } static none: Effects task: Task } class Perform extends Effects { constructor(task: Task) { super() this.task = task } map(f: (a: a) => b): Effects { return new Perform(this.task.map(f)) } execute(address: Address): Task { return this.task.chain(value => Task.send(address, value)) } } class None extends Effects { map(f: (a: a) => b): Effects { return Effects.none } execute(address: Address): Task { return nil } } Effects.none = new None() class Batch extends Effects { constructor(effects: Array>) { super() this.effects = effects } map(f: (a: a) => b): Effects { return new Batch(this.effects.map(effect => effect.map(f))) } execute(address: Address): Task { return new Task((succeed, fail) => { const { effects } = this const count = effects.length let index = 0 while (index < count) { const effect = effects[index] if (!(effect instanceof None)) { Task.fork(effect.execute(address), ignore, raise) } index = index + 1 } succeed(void 0) }) } effects: Array> } ================================================ FILE: src/html.js ================================================ /* @flow */ import { Node } from "reflex-driver" import type { Properties } from "reflex-driver" import { element } from "./dom" type Element = ( properties: ?Properties, children: ?Array ) => Node export type Html = { a: Element, abbr: Element, address: Element, area: Element, article: Element, aside: Element, audio: Element, b: Element, base: Element, bdi: Element, bdo: Element, big: Element, blockquote: Element, body: Element, br: Element, button: Element, canvas: Element, caption: Element, cite: Element, code: Element, col: Element, colgroup: Element, data: Element, datalist: Element, dd: Element, del: Element, details: Element, dfn: Element, dialog: Element, div: Element, dl: Element, dt: Element, em: Element, embed: Element, fieldset: Element, figcaption: Element, figure: Element, footer: Element, form: Element, h1: Element, h2: Element, h3: Element, h4: Element, h5: Element, h6: Element, head: Element, header: Element, hr: Element, html: Element, i: Element, iframe: Element, img: Element, input: Element, ins: Element, kbd: Element, keygen: Element, label: Element, legend: Element, li: Element, link: Element, main: Element, map: Element, mark: Element, menu: Element, menuitem: Element, meta: Element, meter: Element, nav: Element, noscript: Element, object: Element, ol: Element, optgroup: Element, option: Element, output: Element, p: Element, param: Element, picture: Element, pre: Element, progress: Element, q: Element, rp: Element, rt: Element, ruby: Element, s: Element, samp: Element, script: Element, section: Element, select: Element, small: Element, source: Element, span: Element, strong: Element, style: Element, sub: Element, summary: Element, sup: Element, table: Element, tbody: Element, td: Element, textarea: Element, tfoot: Element, th: Element, thead: Element, time: Element, title: Element, tr: Element, track: Element, u: Element, ul: Element, var: Element, video: Element, wbr: Element, circle: Element, clipPath: Element, defs: Element, ellipse: Element, g: Element, line: Element, linearGradient: Element, mask: Element, path: Element, pattern: Element, polygon: Element, polyline: Element, radialGradient: Element, rect: Element, stop: Element, svg: Element, text: Element, tspan: Element } export const html: Html = [ "a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr", "circle", "clipPath", "defs", "ellipse", "g", "line", "linearGradient", "mask", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "stop", "svg", "text", "tspan" ].reduce((html, tagName) => { const create: Element = (properties, children) => element(tagName, properties, children) html[tagName] = create return html }, ((Object.create(null): any): Html)) ================================================ FILE: src/preemptive-animation-frame.js ================================================ /* @flow */ type Time = number type State = 0 | 1 | 2 // Invariants: // 1. In the NO_REQUEST state, there is never a scheduled animation frame. // 2. In the PENDING_REQUEST and EXTRA_REQUEST states, there is always exactly // one scheduled animation frame. const NO_REQUEST = 0 const PENDING_REQUEST = 1 const EXTRA_REQUEST = 2 let nextID: number = 0 let state: State = NO_REQUEST let requests: Array<(time: Time) => any> = [] let ids: Array = [] const absent = new String("absent") export const requestAnimationFrame = (request: (time: Time) => a) => { if (state === NO_REQUEST) { window.requestAnimationFrame(performAnimationFrame) } const id = ++nextID requests.push(request) ids.push(id) state = PENDING_REQUEST return id } export const cancelAnimationFrame = (id: number): void => { const index = ids.indexOf(id) if (index >= 0) { ids.splice(index, 1) requests.splice(index, 1) } } export const forceAnimationFrame = (time: Time = window.performance.now()) => performAnimationFrame(time) const performAnimationFrame = (time: Time) => { switch (state) { case NO_REQUEST: // This state should not be possible. How can there be no // request, yet somehow we are actively fulfilling a // request? throw Error(`Unexpected frame request`) case PENDING_REQUEST: // At this point, we do not *know* that another frame is // needed, but we make an extra frame request just in // case. It's possible to drop a frame if frame is requested // too late, so we just do it preemptively. window.requestAnimationFrame(performAnimationFrame) state = EXTRA_REQUEST ids.splice(0) dispatchAnimationFrame(requests.splice(0), 0, time) break case EXTRA_REQUEST: // Turns out the extra request was not needed, so we will // stop requesting. No reason to call it all the time if // no one needs it. state = NO_REQUEST break } } const dispatchAnimationFrame = ( requests: Array<(time: Time) => a>, index: number, time: Time ) => { let exception: String | Error = absent const count = requests.length try { while (index < count) { const request = requests[index] index = index + 1 request(time) } } catch (error) { exception = error } if (index < count) { dispatchAnimationFrame(requests, index, time) } if (exception != absent) { throw exception } } ================================================ FILE: src/reflex.js ================================================ /* @flow */ import type { Node, Properties, Style, Attributes } from "reflex-driver" export type DOM = Node export { Node, Driver } from "reflex-driver" export type { Address } from "./signal" export type { Properties, Attributes, Style } from "reflex-driver" export { Subscription, subscribe, unsubscribe } from "./subscription" export type { Application, AdvancedConfiguration, BeginnerConfiguration } from "./application" export type { Init, Update, View } from "./application" export { forward } from "./signal" export { element, elementNS, text, thunk } from "./dom" export { html } from "./html" export { start, beginner } from "./application" export { Task } from "./task" export { Effects } from "./effects" ================================================ FILE: src/signal.js ================================================ /* @flow */ export type Address = (input: a) => void const Forward = (address: Address, tag: (a: a) => b): Address => { const forward = (message: a) => address(tag(message)) forward.to = address forward.tag = tag return forward } if (global["reflex/address"] == null) { global["reflex/address"] = 0 } // Create a new address. This address will tag each message it receives and then // forward it along to the given address. // Example: // // const Remove = target => {type: "Remove", target} // removeAddress = forward(address, Remove) // // Above example created `removeAddress` tags each message with `Remove` tag // before forwarding them to a general `address`. export const forward = ( address: Address, tag: (value: b) => a ): Address => { // Genrate ID for each address that has a forwarding addresses so that // forwarding addresses could be cached by that id and a tag-ing function. const id = address.id != null ? address.id : (address.id = global["reflex/address"]++) const key = `reflex/address/${id}` return tag[key] || (tag[key] = Forward(address, tag)) } ================================================ FILE: src/subscription.js ================================================ /* @flow */ import { Effects } from "./effects" import type { Address } from "./signal" type Tagger = (value: a) => b export interface Subscriber { detail: info, tagger: Tagger } export type Service = { inbox: Address, subscribers: Array>, state: model, feed: Feed } export interface Feed { address?: string, init(): model, subscribe(subscribers: Array>): inn, update( state: model, input: inn, outbox: Address ): [model, Effects] } export class Subscription { static none: Subscription<*> static batch(subscriptions: Array>): Subscription { return new Batch(subscriptions) } map(tag: (value: a) => b): Subscription { return this.map(tag) } reduce( reducer: (result: state, input: Subscribe) => state, init: state ): state { return init } } export class Subscribe extends Subscription implements Subscriber { feed: Feed detail: info tagger: Tagger constructor( feed: Feed, detail: info, tagger: Tagger ) { super() this.feed = feed this.detail = detail this.tagger = tagger } map(tag: (value: out) => b): Subscription { const decoder = (value: inn): b => tag(this.tagger(value)) return new Subscribe(this.feed, this.detail, decoder) } reduce( reducer: ( result: state, input: Subscribe ) => state, init: state ): state { return reducer(init, this) } } class Batch extends Subscription { constructor(subscriptions: Array>) { super() this.subscriptions = subscriptions } map(tag: (value: a) => b): Subscription { const subscriptions = this.subscriptions.map($ => $.map(tag)) return new Batch(subscriptions) } reduce( reducer: (result: state, input: Subscribe) => state, init: state ): state { return this.subscriptions.reduce( (result: state, subscription: Subscription): state => subscription.reduce(reducer, result), init ) } subscriptions: Array> } export const subscribe = ( feed: Feed, detail: info, tagger: Tagger ): Subscription => new Subscribe(feed, detail, tagger) const none: Subscription = new Batch([]) export const unsubscribe = (_: mixed): Subscription => none Subscription.none = none ================================================ FILE: src/task.js ================================================ /* @flow */ import type { Address } from "./signal" import { requestAnimationFrame, cancelAnimationFrame } from "./preemptive-animation-frame" export type ThreadID = number export type Time = number export type ProcessID = number const raise = error => { throw Error( `Task was not supposet to never fail but it did fail with error ${error}` ) } const ignore = _ => void 0 export interface Process { id: ProcessID, isActive: boolean, kill(reson: reason): void } export class Task { static create( execute: (succeed: (a: a) => void, fail: (x: x) => void) => void ): Task { console.warn("Task.create is deprecated API use new Task instead") return new Task(execute) } static future(request: () => Promise): Task { console.warn("Task.future is deprecated API use new Task instead") return new Future(request) } static succeed(value: a): Task { return new Succeed(value) } static fail(error: x): Task { return new Fail(error) } static spawn(task: Task): Task { return new Spawn(task) } static sleep(time: Time): Task { return new Sleep(time) } static requestAnimationFrame(): Task { return new AnimationFrame() } static send(address: Address, message: a): Task { return new Send(address, message) } static fork( task: Task, onSucceed: (a: a) => void, onFail: (x: x) => void ): Process { return Thread.fork(task, onSucceed, onFail) } static perform(task: Task): void { Thread.fork(task, ignore, raise) } constructor( execute: ?(succeed: (a: a) => void, fail: (x: x) => void) => handle, cancel: ?(handle: handle) => void ) { this.type = "Task" const task = (this: any) if (execute != null) { task.fork = execute } if (cancel != null) { task.abort = cancel } } chain(next: (a: a) => Task): Task { return new Chain(this, next) } map(f: (input: a) => b): Task { return new Map(this, f) } capture(handle: (error: x) => Task): Task { return new Capture(this, handle) } format(f: (input: x) => y): Task { return new Format(this, f) } recover(regain: (error: x) => a): Task { return new Recover(this, regain) } fork(succeed: (a: a) => void, fail: (x: x) => void): * { return this.execute(succeed, fail) } abort(token: *): void { return this.cancel(token) } type: * execute: (succeed: (a: a) => void, fail: (x: x) => void) => * cancel: (handle: *) => void } class Succeed extends Task { constructor(value: a) { super() this.type = "Succeed" this.value = value } fork(succeed: (a: a) => void, fail: (x: x) => void): void { succeed(this.value) } type: "Succeed" value: a } class Fail extends Task { constructor(error: x) { super() this.type = "Fail" this.error = error } fork(succeed: (a: a) => void, fail: (x: x) => void): void { fail(this.error) } type: "Fail" error: x } class Sleep extends Task { constructor(time: Time) { super() this.time = time } fork(succeed: (a: a) => void, fail: (x: x) => void): number { return setTimeout(succeed, this.time, void 0) } abort(id: number): void { clearTimeout(id) } time: Time } class AnimationFrame extends Task { constructor() { super() } fork(succeed: (a: Time) => void, fail: (x: x) => void): number { return requestAnimationFrame(succeed) } abort(id: number): void { cancelAnimationFrame(id) } } let threadID = 0 class Spawn extends Task { constructor(task: Task) { super() this.task = task } fork(succeed: (a: ThreadID) => void, fail: (x: y) => void): void { Promise.resolve(null).then(_ => Task.fork(this.task, noop, noop)) succeed(++threadID) } task: Task } class Send extends Task { constructor(address: Address, message: a) { super() this.message = message this.address = address } fork(succeed: (a: void) => void, fail: (x: x) => void): void { succeed(void this.address(this.message)) } message: a address: Address } class Future extends Task { constructor(request: () => Promise) { super() this.request = request } fork(succeed: (a: a) => void, fail: (x: x) => void): void { this.request().then(succeed, fail) } request: () => Promise } class Then extends Task { constructor(task: Task) { super() this.type = "Then" this.task = task } fork(succeed: (value: b) => void, fail: (error: x) => void): void { this.task.fork( (value: a): void => void this.next(value).fork(succeed, fail), fail ) } next(input: a): Task { throw Error("Subclass of absract Then must implement next method") } type: "Then" task: Task } class Chain extends Then { constructor(task: Task, next: (input: a) => Task) { super(task) this.chainer = next } next(input: a): Task { return this.chainer(input) } chainer: (input: a) => Task } class Map extends Then { constructor(task: Task, mapper: (input: a) => b) { // Note: Had to trick flow into thinking that `Format.prototype.handle` was // passed, otherwise it fails to infer polymorphic nature. super(task) this.mapper = mapper } next(input: a): Task { return new Succeed(this.mapper(input)) } mapper: (input: a) => b } class Catch extends Task { constructor(task: Task) { super() this.type = "Catch" this.task = task } fork(succeed: (value: a) => void, fail: (error: y) => void): void { this.task.fork( succeed, error => void this.handle(error).fork(succeed, fail) ) } handle(error: x): Task { throw Error("Subclass of absract Catch must implement handle method") } type: "Catch" task: Task } class Capture extends Catch { constructor(task: Task, handle: (error: x) => Task) { super(task) this.capturer = handle } handle(error: x): Task { return this.capturer(error) } capturer: (error: x) => Task } class Recover extends Catch { constructor(task: Task, regain: (error: x) => a) { super(task) this.regain = regain } handle(error: x): Task { return new Succeed(this.regain(error)) } regain: (error: x) => a } class Format extends Catch { constructor(task: Task, formatter: (error: x) => y) { super(task) this.formatter = formatter } handle(error: x): Task { return new Fail(this.formatter(error)) } formatter: (error: x) => y } const noop = () => void 0 let nextID = 0 type Root = | Succeed | Fail | Then | Catch<*, x, a> | Task class Thread { id: ProcessID root: Root<*, *> stack: Array | Then<*, *, *>> position: number mailbox: Array abortHandle: * isActive: boolean succeed: (input: value) => void fail: (error: error) => void isPending: boolean isPaused: boolean success: ?Succeed<*, *> failure: ?Fail<*, *> onSucceed: (input: value) => void onFail: (error: error) => void static fork( task: Task, onSucceed: (input: value) => void, onFail: (error: error) => void ): Process { const process = new Thread() process.id = ++nextID process.position = 0 process.root = task process.stack = [] process.mailbox = [] process.abortHandle = null process.isActive = true process.isPending = false process.isPaused = true process.success = null process.failure = null process.succeed = onSucceed process.fail = onFail process.onSucceed = process.onSucceed.bind(process) process.onFail = process.onFail.bind(process) process.schedule() return process } onSucceed(ok) { if (this.isPending) { this.isPending = false this.abortHandle = null if (this.success != null) { this.success.value = ok } else { this.success = new Succeed(ok) } this.root = this.success this.schedule() } } onFail(failure) { if (this.isPending) { this.isPending = false this.abortHandle = null if (this.failure != null) { this.failure.error = failure } else { this.failure = new Fail(failure) } this.root = this.failure this.schedule() } } kill(exit: reason) { if (this.isActive) { this.isActive = false if (this.root.abort) { this.root.abort(this.abortHandle) } } } schedule() { if (this.isPaused) { this.isPaused = false this.step() } } step() { const process = this while (process.isActive) { const root = process.root switch (root.type) { case "Succeed": { const task: Succeed<*, *> = (root: any) // If task succeeded skip all the error handling. while ( process.position < process.stack.length && process.stack[process.position] instanceof Catch ) { process.position++ } // If end of the stack is reached then break if (process.position >= process.stack.length) { if (process.succeed != null) { process.succeed(task.value) } return } // Otherwise step into next task. const then = process.stack[process.position++] if (then instanceof Then) { process.root = then.next(task.value) } break } case "Fail": { const task: Fail<*, *> = (root: any) // If task fails skip all the chaining. while ( process.position < process.stack.length && process.stack[process.position] instanceof Then ) { process.position++ } // If end of the stack is reached then break. if (this.position >= process.stack.length) { if (process.fail != null) { process.fail(task.error) } return } // Otherwise step into next task. const _catch = process.stack[process.position++] if (_catch instanceof Catch) { process.root = _catch.handle(task.error) } break } case "Then": { const task: Then<*, *, *> = (root: any) if (process.position === 0) { process.stack.unshift(task) } else { process.stack[--process.position] = task } process.root = task.task break } case "Catch": { const task: Catch<*, *, *> = (root: any) if (process.position === 0) { process.stack.unshift(task) } else { process.stack[--process.position] = task } process.root = task.task break } default: { const task = root process.isPending = true process.abortHandle = task.fork(process.onSucceed, process.onFail) process.isPaused = process.isPending if (this.isPending) { return } break } } } } } ================================================ FILE: test/test-api.js ================================================ import test from "blue-tape" import * as Reflex from "../" test("test exported api", async assert => { assert.ok(typeof Reflex.node, "function") assert.ok(typeof Reflex.html, "object") assert.ok(typeof Reflex.html.div, "function") assert.ok(typeof Reflex.thunk, "function") assert.ok(typeof Reflex.send, "function") assert.ok(typeof Reflex.forward, "function") assert.ok(typeof Reflex.Application, "function") assert.ok(typeof Reflex.Task.succeed, "function") assert.ok(typeof Reflex.Task.fail, "function") assert.ok(typeof Reflex.Task.io, "function") assert.ok(typeof Reflex.Task.onSuccess, "function") assert.ok(typeof Reflex.Task.onFailure, "function") assert.ok(typeof Reflex.Task.perform, "function") assert.ok(typeof Reflex.Task.run, "function") })