Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asynchronous on change handlers #29

Merged
merged 4 commits into from
Mar 9, 2015
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
68 changes: 67 additions & 1 deletion spec/cursor_spec.ls
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require! '../src/cursor'
require! <[ ../src/cursor bluebird ]>

{map} = require 'prelude-ls'

Expand Down Expand Up @@ -191,3 +191,69 @@ describe "cursor" (_) ->

expect trace .to-equal [36, 36, 37, 37, 38, 38, 39, 39, 40, 40]

describe "update transactions", (_) ->
it "starts" ->
data = cursor raw-data
transaction = data.start-transaction!

expect transaction .not.to-be undefined

it "ends and returns a promise" ->
data = cursor raw-data

transaction = data.start-transaction!
promise = data.end-transaction transaction

expect promise .not.to-be undefined
expect promise.then .not.to-be undefined

it "throws when transaction isn't running" ->
data = cursor raw-data

expect(-> data.end-transaction "foo").to-throw new Error "Transaction isn't running"

it "waits for a promise returned by an on-change handler" (done) ->
data = cursor raw-data .get \person.first_name
log = []

data.on-change ->
log.push "first"
bluebird.delay 0
.then !->
log.push "second"

transaction = data.start-transaction!
data.update -> "Ringo"

data.end-transaction transaction
.then ->
expect log .to-equal ["first", "second"]
.finally done

expect log .to-equal ["first"]

it "waits for multiple promises returned by different on-change handlers" (done) ->
data = cursor raw-data .get \person.first_name
log = []

data.on-change ->
log.push "first"
bluebird.delay 0
.then !->
log.push "third"

data.on-change ->
log.push "second"
bluebird.delay 0
.then !->
log.push "fourth"

transaction = data.start-transaction!
data.update -> "Ringo"

data.end-transaction transaction
.then ->
expect log .to-equal ["first", "second", "third", "fourth"]
.finally done

expect log .to-equal ["first", "second"]
43 changes: 26 additions & 17 deletions src/application.ls
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module.exports =
[route-component, context, _] = routes.resolve path, config.routes!
root-element = app-component initial-state: initial-state, component: route-component, context: context

config.start initial-state, (->)
config.start initial-state

root = React.render root-element, root-dom-node

Expand All @@ -40,29 +40,38 @@ module.exports =
# render a particular route to string
# returns a promise of [state, body]
render: (path) ->
new bluebird (res, rej) ->
initial-state = cursor config.get-initial-state!
app-state = cursor config.get-initial-state!

[route-component, context, route-init] = routes.resolve path, config.routes!
root-element = app-component initial-state: initial-state, component: route-component, context: context
[route-component, context, route-init] = routes.resolve path, config.routes!
root-element = app-component initial-state: app-state, component: route-component, context: context

config.start initial-state, ->
return res [initial-state.deref!, React.render-to-string root-element] unless route-init
transaction = app-state.start-transaction!

route-init initial-state, context, ->
res [initial-state.deref!, React.render-to-string root-element]
config.start app-state
route-init app-state, context if route-init

app-state.end-transaction transaction
.then ->
[app-state.deref!, React.render-to-string root-element]

# process a form from a particular route and render to string
# returns a promise of [state, body, location]
process-form: (path, post-data) ->
new bluebird (res, rej) ->
initial-state = cursor config.get-initial-state!
app-state = cursor config.get-initial-state!

[route-component, context, route-init] = routes.resolve path, config.routes!
root-element = app-component initial-state: app-state, component: route-component, context: context

transaction = app-state.start-transaction!

config.start app-state
route-init app-state, context if route-init

location = server-rendering.process-form root-element, app-state, post-data, path

[route-component, context, route-init] = routes.resolve path, config.routes!
root-element = app-component initial-state: initial-state, component: route-component, context: context
app-state.end-transaction transaction
.then ->
body = unless location then React.render-to-string root-element else null

config.start initial-state, ->
return res server-rendering.process-form root-element, initial-state, post-data, path unless route-init
[app-state.deref!, body, location]

route-init initial-state, context, ->
res server-rendering.process-form root-element, initial-state, post-data, path
35 changes: 31 additions & 4 deletions src/cursor.ls
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Immutable = require 'immutable'
require! 'bluebird'

{map, take, reverse, each, join, split, is-type, empty} = require 'prelude-ls'

UpdateTransaction = !->
@promises = []

is-promise = ->
it and it.then and typeof! it.then is 'Function'

# wraps array in a cursor
array-cursor = (root, data, len, path) ->
array = [0 til len] |> map ->
Expand All @@ -10,6 +18,7 @@ array-cursor = (root, data, len, path) ->
array._root = root or this
array._data = if data then Immutable.fromJS(data) else null
array._listeners = {}
array._transactions = []
array._updates = []

# Support all the cursor API
Expand All @@ -20,7 +29,7 @@ array-cursor = (root, data, len, path) ->
object-cursor = (root, data, path) ->
new Cursor root, data, path

notify-listeners = (listeners, path, new-data) !->
notify-listeners = (listeners, transactions, path, new-data) !->
paths = [0 to path.length]
|> map (-> path |> take it)
|> reverse
Expand All @@ -33,7 +42,10 @@ notify-listeners = (listeners, path, new-data) !->
payload = new-data.get-in path
payload .= toJS! if payload.toJS

it(payload)
maybe-promise = it(payload)

if is-promise maybe-promise
transactions |> each -> it.promises.push maybe-promise

flush-updates = (updates) ->
while updates.length > 0
Expand All @@ -60,13 +72,14 @@ perform-update = (cursor, update) ->
cursor._root._swap new-data

# Notify about the change
notify-listeners cursor._root._listeners, cursor._path, new-data
notify-listeners cursor._root._listeners, cursor._root._transactions, cursor._path, new-data

Cursor = (root, data, path) ->
@_path = path
@_root = root or this
@_data = if data then Immutable.fromJS(data) else null
@_listeners = {}
@_transactions = []
@_updates = []

this
Expand Down Expand Up @@ -96,7 +109,7 @@ Cursor.prototype.update = (cbk) ->
updates.push [this, cbk]
return unless updates.length < 2

while updates.length > 0
until empty updates
[cursor, update] = updates[0]
perform-update cursor, update

Expand All @@ -112,6 +125,20 @@ Cursor.prototype.on-change = (cbk) ->
@_root._listeners[key] ||= []
@_root._listeners[key].push cbk

Cursor.prototype.start-transaction = ->
t = new UpdateTransaction!
@_root._transactions.push t

t

Cursor.prototype.end-transaction = (transaction) ->
i = @_root._transactions.index-of transaction
throw new Error "Transaction isn't running" if i < 0

return bluebird.resolve! if empty transaction.promises

bluebird.all transaction.promises

module.exports = (data) ->
if is-type 'Array', data
array-cursor null, data, data.length, []
Expand Down
13 changes: 5 additions & 8 deletions src/server-rendering.ls
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ submit-form = (form) ->
form.props.on-submit fake-event form
ReactUpdates.flushBatchedUpdates!

# Processes a form server-side, returns a redirect location or null
# FIXME should we deal with the redirect in application.ls?
process-form = (root-element, initial-state, post-data, path) ->
configure-react!
reset-redirect!
Expand All @@ -88,20 +90,15 @@ process-form = (root-element, initial-state, post-data, path) ->

[form, inputs] = extract-elements path, post-data, instance

# trigger on-change handlers
# trigger handlers
change-inputs inputs, post-data

# trigger on-submit handler
submit-form form

# end of magic

return [null, null, that] if redirect-location

state = initial-state.deref!
body = React.render-to-string root-element
return that if redirect-location

[state, body, null]
null

reset-redirect = ->
redirect-location := null
Expand Down
4 changes: 2 additions & 2 deletions src/server.ls
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ module.exports = (options=defaults) ->
app = options.app or require options.paths.app.rel

get = (req, res) ->
console.log "GET ", req.original-url
console.log "GET", req.original-url
reflex-get app, req.original-url, options
.then ->
res.send it

post = (req, res) ->
post-data = req.body
console.log "POST ", req.original-url, post-data
console.log "POST", req.original-url, post-data

reflex-post app, req.original-url, post-data, options
.spread (status, headers, body) ->
Expand Down