diff --git a/spec/cursor_spec.ls b/spec/cursor_spec.ls index d035162..ede62a7 100644 --- a/spec/cursor_spec.ls +++ b/spec/cursor_spec.ls @@ -1,4 +1,4 @@ -require! '../src/cursor' +require! <[ ../src/cursor bluebird ]> {map} = require 'prelude-ls' @@ -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"] diff --git a/src/application.ls b/src/application.ls index 5b53c0d..d54733b 100644 --- a/src/application.ls +++ b/src/application.ls @@ -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 @@ -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 diff --git a/src/cursor.ls b/src/cursor.ls index af50f6b..30d0d21 100644 --- a/src/cursor.ls +++ b/src/cursor.ls @@ -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 -> @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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, [] diff --git a/src/server-rendering.ls b/src/server-rendering.ls index 8db001f..2f86499 100644 --- a/src/server-rendering.ls +++ b/src/server-rendering.ls @@ -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! @@ -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 diff --git a/src/server.ls b/src/server.ls index 41faf4b..f9ce998 100644 --- a/src/server.ls +++ b/src/server.ls @@ -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) ->