From 162ec3c8d73c649e5dc87cd5e063628584e9be54 Mon Sep 17 00:00:00 2001 From: Viktor Charypar Date: Thu, 5 Mar 2015 18:44:29 +0000 Subject: [PATCH 1/4] Add support for async on-change handlers --- spec/cursor_spec.ls | 68 ++++++++++++++++++++++++++++++++++++++++++++- src/cursor.ls | 32 ++++++++++++++++++--- 2 files changed, 95 insertions(+), 5 deletions(-) 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/cursor.ls b/src/cursor.ls index af50f6b..f9f693d 100644 --- a/src/cursor.ls +++ b/src/cursor.ls @@ -1,6 +1,11 @@ Immutable = require 'immutable' +require! 'bluebird' + {map, take, reverse, each, join, split, is-type, empty} = require 'prelude-ls' +UpdateTransaction = !-> + @promises = [] + # wraps array in a cursor array-cursor = (root, data, len, path) -> array = [0 til len] |> map -> @@ -10,6 +15,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 +26,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 +39,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 maybe-promise and maybe-promise.then + transactions |> each -> it.promises.push maybe-promise flush-updates = (updates) -> while updates.length > 0 @@ -60,13 +69,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 +106,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 +122,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, [] From e0ecb7bf8dd3e7a04aae5fffa4d755622fb0dafa Mon Sep 17 00:00:00 2001 From: Viktor Charypar Date: Fri, 6 Mar 2015 19:48:00 +0000 Subject: [PATCH 2/4] Use on-change transactions when rendering server side --- src/application.ls | 43 +++++++++++++++++++++++++---------------- src/server-rendering.ls | 9 ++++----- 2 files changed, 30 insertions(+), 22 deletions(-) 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/server-rendering.ls b/src/server-rendering.ls index 8db001f..be20352 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! @@ -96,12 +98,9 @@ process-form = (root-element, initial-state, post-data, path) -> # end of magic - return [null, null, that] if redirect-location + return that if redirect-location - state = initial-state.deref! - body = React.render-to-string root-element - - [state, body, null] + null reset-redirect = -> redirect-location := null From c7370c91db8381c3cae5ff49b467e67e34453543 Mon Sep 17 00:00:00 2001 From: Viktor Charypar Date: Fri, 6 Mar 2015 19:48:12 +0000 Subject: [PATCH 3/4] Minor cleanup --- src/server-rendering.ls | 4 +--- src/server.ls | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server-rendering.ls b/src/server-rendering.ls index be20352..2f86499 100644 --- a/src/server-rendering.ls +++ b/src/server-rendering.ls @@ -90,10 +90,8 @@ 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 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) -> From 0cd9557ac958c563d4cd958920cbe401b8ad5adf Mon Sep 17 00:00:00 2001 From: Viktor Charypar Date: Sun, 8 Mar 2015 16:11:59 +0000 Subject: [PATCH 4/4] Better check for a thenable --- src/cursor.ls | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cursor.ls b/src/cursor.ls index f9f693d..30d0d21 100644 --- a/src/cursor.ls +++ b/src/cursor.ls @@ -6,6 +6,9 @@ require! 'bluebird' 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 -> @@ -41,7 +44,7 @@ notify-listeners = (listeners, transactions, path, new-data) !-> maybe-promise = it(payload) - if maybe-promise and maybe-promise.then + if is-promise maybe-promise transactions |> each -> it.promises.push maybe-promise flush-updates = (updates) ->