diff --git a/src/re_frame/alpha.cljc b/src/re_frame/alpha.cljc deleted file mode 100644 index b5bc239c..00000000 --- a/src/re_frame/alpha.cljc +++ /dev/null @@ -1,1398 +0,0 @@ -(ns re-frame.alpha - (:require - [re-frame.events :as events] - [re-frame.subs :as subs] - [re-frame.subs.alpha :as subs.alpha] - [re-frame.interop :as interop] - [re-frame.db :as db] - [re-frame.fx :as fx] - [re-frame.cofx :as cofx] - [re-frame.router :as router] - [re-frame.settings :as settings] - [re-frame.loggers :as loggers] - [re-frame.registrar :as registrar] - [re-frame.register.alpha :as register.alpha] - [re-frame.interceptor :as interceptor] - [re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor - fx-handler->interceptor - ctx-handler->interceptor]] - [re-frame.flow.alpha :as flow] - [re-frame.utils :as utils] - [clojure.set :as set])) - -(defn reg - "Register a handler. - - `kind`: what kind of handler to register. Possible vals: - - ** :sub-lifecycle ** - - UNDOCUMENTED. See https://github.com/day8/re-frame/issues/680. - - ** :legacy-sub ** - - `(reg :sub query-id signal computation-fn)` - - Register a `signal-fn` and a `computation-fn` for a given `query-id`. - `computation-fn` is a function of `[signals query-vector]`. - See `:sub` for an explanation of all the arguments. - - Compatibility: - - Call `sub` to invoke the `computation-fn`. If passed a query-map, - `sub` converts it to a query-vector. - - If a `:re-frame/query-v` key is present, `sub` uses it - for the query-vector. Otherwise it uses the `:re-frame/q` - key to build a 1-item vector. `sub` includes the original - query-map in the metadata with a `:re-frame/query-m` key. - - For instance, - `#!clj {::rf/q ::items}` - converts to: - `#!clj ^{::rf/query-m {::rf/q ::items}} [::items]` - - #!clj - {::rf/q ::items - ::rf/lifecycle :reactive - ::rf/query-v [::items 1 2 3]} - - - converts to: - - #!clj - ^{::rf/query-m {::rf/q ::items - ::rf/lifecycle :reactive}} - [::items 1 2 3] - - ** :sub ** - - `(reg :sub query-id signal computation-fn)` - - Register a `signal-fn` and a `computation-fn` for a given `query-id`. - `computation-fn` is a function of `[signals query-map]`. - Call `sub` to invoke the handler. If passed a query-vector, - re-frame converts it to a query map, merging in the value of - `:re-frame/query-m` from the metadata, and including - the original query-vector with a `:re-frame/query-v` key. - - For instance, - `#!clj ^{:rf/query-m {:rf/lifecycle :reactive}} [::items 1 2 3]` - converts to: - - #!clj - {::rf/q ::items - ::rf/lifecycle :reactive - ::rf/query-v [::items 1 2 3]} - - For convenience, re-frame also merges in the `:re-frame/lifecycle` - key from the metadata. For instance, - `#!clj ^{:rf/lifecycle :reactive} [::items 1 2 3]` - converts to: - - #!clj - {:rf/q ::items - :rf/lifecycle :reactive - :rf/query-v [::items 1 2 3]} - - A call to `(reg :sub query-id signal-fn computation-fn)` - associates a `query-id` WITH two functions. - - The two functions provide 'a mechanism' for creating a node - in the Signal Graph. When a node of type `query-id` is needed, - the two functions can be used to create it. - - - `query-id` - typically a namespaced keyword (later used in subscribe). - - `signal-fn` - optional. Returns the input data-flows required by this kind of node. - - `computation-fn` - computes the value (output) of the node (from the input data flows). - - Later, during app execution, a call to `(sub {:re-frame/q :sub-id :color :blue})` - will trigger the need for a new `:sub-id` Signal Graph node (matching the - query `{:re-frame/q :sub-id :color :blue}`). And, to create that node the two functions - associated with `:sub-id` will be looked up and used. - - Just to be clear: calling `reg :sub` does not immediately create a node. - It only registers 'a mechanism' (the two functions) by which nodes - can be created later, when a node is bought into existence by the - use of `sub`. - - The `computation-fn` is always the last argument supplied and has three ways to be called. - Two of these ways are syntactic sugar to provide easier access to functional abstractions around your data. - - 1. A function that will accept two parameters, the `input-values` and `query`. This is the - standard way to provide a `computation-function` - - #!clj - (reg-sub - :query-id - (fn [input-values query] - (:foo input-values))) - - 2. A single sugary tuple of `:->`and a 1-arity `computation-function`: - - #!clj - (reg-sub - :query-id - :-> computation-fn) - - This sugary variation allows you to pass a function that will expect only one parameter, - namely the `input-values`, and entirely omit the `query`. A typical `computation-function` - expects two parameters which can cause unfortunate results when attempting to use - clojure standard library functions, or other functions, in a functional manner. - - For example, a significant number of subscriptions exist only to get a value - from the `input-values`. As shown below, this subscription will simply retrieve - the value associated with the `:foo` key in our db: - - #!clj - (reg-sub - :query-id - (fn [db _] ;; :<---- trivial boilerplate we might want to skip over - (:foo db))) - - This is slightly more boilerplate than we might like to do, - as we can use a keyword directly as a function, and we might like to do this: - - #!clj - (reg-sub - :query-id - :foo) ;; :<---- This could be dangerous. If `:foo` is not in db, we get the `query-vector` instead of `nil`. - - By using `:->` our function would not contain the `query-vector`, and any - missing keys would be represented as such: - - #!clj - (reg-sub - :query-id - :-> :foo) - - This form allows us to ignore the `query` if our `computation-fn` - has no need for it, and be safe from any accidents. Any 1-arity function can be provided, - and for more complicated use cases, `partial`, `comp`, and anonymous functions can still be used. - - 3. A single sugary tuple of `:=>` and a multi-arity `computation-function` - - #!clj - (reg-sub - :query-id - :=> computation-fn) - - A vector `query` can be broken into two components `[query-id & optional-values]`, and - some subscriptions require the `optional-values` for extra work within the subscription. - To use them in variation #1, we need to destructure our `computation-fn` parameters - in order to use them. - - #!clj - (reg-sub - :query-id - (fn [db [_ foo]] - [db foo])) - - Again we are writing boilerplate just to reach our values, and we might prefer to - have direct access through a parameter vector like `[input-values optional-values]` - instead, so we might be able to use a multi-arity function directly as our `computation-fn`. - A rewrite of the above sub using this sugary syntax would look like this: - - #!clj - (reg-sub - :query-id - :=> vector) ;; :<---- Could also be `(fn [db foo] [db foo])` - - If handling a map query, `:=>` simply uses the entire map. - - The `computation function` is expected to take two arguments: - - - `input-values` - the values which flow into this node (how is it wired into the graph?) - - `query` - the value passed to `sub` - - and it returns a computed value (which becomes the output of the node) - - When `computation-fn` is called, the 2nd argument, `query`, will be that - value passed to `sub`. So, if the call was `(subscribe {:re-frame/q :sub-id :color :blue})`, - then the `query` supplied to the computation function will be `{:re-frame/q :sub-id :color :blue}`. - - The argument(s) supplied to `reg-sub` between `query-id` and the `computation-function` - can vary in 3 ways, but whatever is there defines the `input signals` part - of `the mechanism`, specifying what input values \"flow into\" the - `computation function` (as the 1st argument) when it is called. - - So, `reg-sub` can be called in one of three ways, because there are three ways - to define the input signals part. But note, the 2nd way, in which a - `signals function` is explicitly supplied, is the most canonical and - instructive. The other two are really just sugary variations. - - **First variation** - no input signal function given: - - #!clj - (reg-sub - :query-id - a-computation-fn) ;; has signature: (fn [db query] ... ret-value) - - In the absence of an explicit `signals function`, the node's input signal defaults to `app-db` - and, as a result, the value within `app-db` (a map) is - given as the 1st argument when `a-computation-fn` is called. - - - **Second variation** - a signal function is explicitly supplied: - - #!clj - (reg-sub - :query-id - signal-fn ;; <-- here - computation-fn) - - This is the most canonical and instructive of the three variations. - - When a node is created from the template, the `signal function` will be called and it - is expected to return the input signal(s) as either a singleton, if there is only - one, or a sequence if there are many, or a map with the signals as the values. - - The current values of the returned signals will be supplied as the 1st argument to - the `a-computation-fn` when it is called - and subject to what this `signal-fn` returns, - this value will be either a singleton, sequence or map of them (paralleling - the structure returned by the `signal function`). - - This example `signal function` returns a 2-vector of input signals. - - #!clj - (fn [query] - [(sub [:a-sub]) - (sub [:b-sub])]) - - The associated computation function must be written - to expect a 2-vector of values for its first argument: - - #!clj - (fn [[a b] query] ;; 1st argument is a seq of two values - ....) - - If, on the other hand, the signal function was simpler and returned a singleton, like this: - - #!clj - (fn [query] - (sub [:a-sub])) ;; <-- returning a singleton - - then the associated computation function must be written to expect a single value - as the 1st argument: - - #!clj - (fn [a query] ;; 1st argument is a single value - ...) - - Further Note: variation #1 above, in which an `signal-fn` was not supplied, like this: - - #!clj - (reg-sub - :query-id - a-computation-fn) ;; has signature: (fn [db query] ... ret-value) - - is the equivalent of using this - 2nd variation and explicitly supplying a `signal-fn` which returns `app-db`: - - #!clj - (reg-sub - :query-id - (fn [_ _] re-frame/app-db) ;; <-- explicit signal-fn - a-computation-fn) ;; has signature: (fn [db query-vec] ... ret-value) - - **Third variation** - syntax Sugar - - #!clj - (reg-sub - :a-b-sub - :<- [:a-sub] - :<- [:b-sub] - (fn [[a b] query] ;; 1st argument is a seq of two values - {:a a :b b})) - - This 3rd variation is just syntactic sugar for the 2nd. Instead of providing an - `signals-fn` you provide one or more pairs of `:<-` and a subscription vector. - - If you supply only one pair a singleton will be supplied to the computation function, - as if you had supplied a `signal-fn` returning only a single value: - - #!clj - (reg-sub - :a-sub - :<- [:a-sub] - (fn [a query] ;; only one pair, so 1st argument is a single value - ...)) - - Syntactic sugar for both the `signal-fn` and `computation-fn` can be used together - and the direction of arrows shows the flow of data and functions. The example from - directly above is reproduced here: - - #!clj - (reg-sub - :a-b-sub - :<- [:a-sub] - :<- [:b-sub] - :-> (partial zipmap [:a :b])) - - For further understanding, read the tutorials, and look at the detailed comments in - /examples/todomvc/src/subs.cljs. - - See also: `sub` - " - {:api-docs/heading "Registration"} - [kind & args] - (apply register.alpha/reg kind args)) - -;; -- dispatch ---------------------------------------------------------------- - -(defn dispatch - "Queue `event` for processing (handling). - - `event` is a vector and the first element is typically a keyword - which identifies the kind of event. - - The event will be added to a FIFO processing queue, so event - handling does not happen immediately. It will happen 'very soon' - but not now. And if the queue already contains events, they - will be processed first. - - Usage: - - #!clj - (dispatch [:order \"pizza\" {:supreme 2 :meatlovers 1 :veg 1}]) - " - {:api-docs/hide true} - [event] - (router/dispatch event)) - -(defn dispatch-sync - "Synchronously (immediately) process `event`. It does **not** queue - the event for handling later as `dispatch` does. - - `event` is a vector and the first element is typically a keyword - which identifies the kind of event. - - It is an error to use `dispatch-sync` within an event handler because - you can't immediately process an new event when one is already - part way through being processed. - - Generally, avoid using this function, and instead, use `dispatch`. - Only use it in the narrow set of cases where any delay in - processing is a problem: - - 1. the `:on-change` handler of a text field where we are expecting fast typing - 2. when initialising your app - see 'main' in examples/todomvc/src/core.cljs - 3. in a unit test where immediate, synchronous processing is useful - - Usage: - - #!clj - (dispatch-sync [:sing :falsetto \"piano accordion\"]) - " - {:api-docs/hide true} - [event] - (router/dispatch-sync event)) - -;; -- Events ------------------------------------------------------------------ - -(defn reg-event-db - "Register the given event `handler` (function) for the given `id`. Optionally, provide - an `interceptors` chain: - - - `id` is typically a namespaced keyword (but can be anything) - - `handler` is a function: (db event) -> db - - `interceptors` is a collection of interceptors. Will be flattened and nils removed. - - Example Usage: - - #!clj - (reg-event-db - :token - (fn [db event] - (assoc db :some-key (get event 2))) ;; return updated db - - Or perhaps: - - #!clj - (reg-event-db - :namespaced/id ;; <-- namespaced keywords are often used - [one two three] ;; <-- a seq of interceptors - (fn [db [_ arg1 arg2]] ;; <-- event vector is destructured - (-> db - (dissoc arg1) - (update :key + arg2)))) ;; return updated db - " - {:api-docs/hide true} - ([id handler] - (reg-event-db id nil handler)) - ([id interceptors handler] - (events/register id [cofx/inject-db - fx/do-fx - flow/interceptor - flow/do-fx - std-interceptors/inject-global-interceptors - interceptors - (db-handler->interceptor handler)]))) - -(defn reg-event-fx - "Register the given event `handler` (function) for the given `id`. Optionally, provide - an `interceptors` chain: - - - `id` is typically a namespaced keyword (but can be anything) - - `handler` is a function: (coeffects-map event-vector) -> effects-map - - `interceptors` is a collection of interceptors. Will be flattened and nils removed. - - - Example Usage: - - #!clj - (reg-event-fx - :event-id - (fn [cofx event] - {:db (assoc (:db cofx) :some-key (get event 2))})) ;; return a map of effects - - - Or perhaps: - - #!clj - (reg-event-fx - :namespaced/id ;; <-- namespaced keywords are often used - [one two three] ;; <-- a seq of interceptors - (fn [{:keys [db] :as cofx} [_ arg1 arg2]] ;; destructure both arguments - {:db (assoc db :some-key arg1) ;; return a map of effects - :fx [[:dispatch [:some-event arg2]]]})) - " - {:api-docs/hide true} - ([id handler] - (reg-event-fx id nil handler)) - ([id interceptors handler] - (events/register id [cofx/inject-db - fx/do-fx - flow/interceptor - flow/do-fx - std-interceptors/inject-global-interceptors - flow/interceptor - interceptors - (fx-handler->interceptor handler)]))) - -(defn reg-event-ctx - "Register the given event `handler` (function) for the given `id`. Optionally, provide - an `interceptors` chain: - - - `id` is typically a namespaced keyword (but can be anything) - - `handler` is a function: context-map -> context-map - - You can explore what is provided in `context` [here](https://day8.github.io/re-frame/Interceptors/#what-is-context). - - Example Usage: - - #!clj - (reg-event-ctx - :event-id - (fn [{:keys [coeffects] :as context}] - (let [initial {:db (:db coeffects) - :event (:event coeffects) - :fx []} - result (-> initial - function1 - function2 - function3) - effects (select-keys result [:db :fx])] - (assoc context :effects effects)))) - " - {:api-docs/hide true} - ([id handler] - (reg-event-ctx id nil handler)) - ([id interceptors handler] - (events/register id [cofx/inject-db - fx/do-fx - flow/interceptor - flow/do-fx - std-interceptors/inject-global-interceptors - interceptors - (ctx-handler->interceptor handler)]))) - -(defn clear-event - "Unregisters event handlers (presumably registered previously via the use of `reg-event-db` or `reg-event-fx`). - - When called with no args, it will unregister all currently registered event handlers. - - When given one arg, assumed to be the `id` of a previously registered - event handler, it will unregister the associated handler. Will produce a warning to - console if it finds no matching registration." - {:api-docs/hide true} - ([] - (registrar/clear-handlers events/kind)) - ([id] - (registrar/clear-handlers events/kind id))) - -;; -- error handler ----------------------------------------------------------- - -(defn reg-event-error-handler - "Register the given event error `handler` (function) that will catch unhandled exceptions - thrown in the interceptors/handler chain. - - Only one `handler` can be registered. Registering a new `handler` clears the existing `handler`. - - This `handler` function has the signature: - - `(handler [original-error re-frame-error])` - - - `original-error`: A plaform-native Error object. - Represents the original error thrown by user code. - - This is the error you see when no `handler` is registered. - To preserve this behavior, simply call `(throw original-error)` - at the end of your handler function. - - - `re-frame-error`: A clojure ExceptionInfo object. - Includes the stacktrace of re-frame's internal functions, - and extra data about the current interceptor context. - Call `(ex-data re-frame-error)` to get this info. - It includes: - - - `:interceptor`: the `:id` of the throwing interceptor. - - `:direction`: `:before` or `:after`. - - `:event-v`: the re-frame event which invoked this interceptor." - {:api-docs/hide true} - [handler] - (registrar/register-handler :error :event-handler handler)) - -(reg-event-error-handler interceptor/default-error-handler) - -;; -- subscriptions ----------------------------------------------------------- - -(defn sub - "Given a `query`, returns a Reagent `reaction` which will, over - time, reactively deliver a stream of values. So, in FRP-ish terms, - it returns a `Signal`. - - To obtain the current value from the Signal, it must be dereferenced: - - #!clj - (let [signal (sub {:re-frame/q ::items}) - value (deref signal)] ;; could be written as @signal - ...) - - which is typically written tersely as simple: - - #!clj - (let [items @(sub {:re-frame/q ::items})] - ...) - - - `query` is a map containing: - `:re-frame/q`: Required. Names the query. Typically a namespaced keyword. - `:re-frame/lifecycle`: Optional. See docs for `reg-sub-lifecycle`. - - The entire `query` is passed to the subscription handler. This means you can use - additional keys to parameterise the query it performs. - - **Example Usage**: - - #!clj - (require '[re-frame :as-alias rf]) - (sub {::rf/q ::items - ::rf/lifecycle ::rf/reactive - :color \"blue\" - :size :small}) - - Note: for any given call to `sub there must have been a previous call - to `reg`, registering the query handler (functions) associated with - `query-id`. - - **De-duplication** - - Two, or more, concurrent subscriptions for the same query will - source reactive updates from the one executing handler. - - See also: `reg-sub` - - **Flows** - - Flows have their own lifecycle, and you don't need to provide an `::rf/lifecycle` key. - To subscribe to a flow, simply call: - - #!clj - (sub :flow {:id :your-flow-id}) - - **Legacy support** - - `dyn-v` is not supported. - " - {:api-docs/heading "Subscription"} - ([q] - (subs.alpha/sub q)) - ([id q] - (subs.alpha/sub id q))) - -(defn clear-sub ;; think unreg-sub - "Unregisters subscription handlers (presumably registered previously via the use of `reg-sub`). - - When called with no args, it will unregister all currently registered subscription handlers. - - When given one arg, assumed to be the `id` of a previously registered - subscription handler, it will unregister the associated handler. Will produce a warning to - console if it finds no matching registration. - - NOTE: Depending on the usecase, it may be necessary to call `clear-subscription-cache!` afterwards" - {:api-docs/hide true} - ([] - (registrar/clear-handlers subs/kind)) - ([query-id] - (registrar/clear-handlers subs/kind query-id))) - -(defn reg-sub-raw - "This is a low level, advanced function. You should probably be - using `reg-sub` instead. - - Some explanation is available in the docs at - http://day8.github.io/re-frame/flow-mechanics/" - {:api-docs/hide true} - [query-id handler-fn] - (registrar/register-handler subs/kind query-id handler-fn)) - -;; XXX -(defn clear-subscription-cache! - "Removes all subscriptions from the cache. - - This function can be used at development time or test time. Useful when hot reloading - namespaces containing subscription handlers. Also call it after a React/render exception, - because React components won't have been cleaned up properly. And this, in turn, means - the subscriptions within those components won't have been cleaned up correctly. So this - forces the issue. - " - {:api-docs/hide true} - [] - (subs/clear-subscription-cache!)) - -;; -- effects ----------------------------------------------------------------- - -(defn reg-fx - "Register the given effect `handler` for the given `id`: - - - `id` is keyword, often namespaced. - - `handler` is a side-effecting function which takes a single argument and whose return - value is ignored. - - To use, first, associate `:effect2` with a handler: - - #!clj - (reg-fx - :effect2 - (fn [value] - ... do something side-effect-y)) - - Then, later, if an event handler were to return this effects map: - - #!clj - {:effect2 [1 2]} - - then the `handler` `fn` we registered previously, using `reg-fx`, will be - called with an argument of `[1 2]`. - " - {:api-docs/hide true} - [id handler] - (fx/reg-fx id handler)) - -(defn clear-fx ;; think unreg-fx - "Unregisters effect handlers (presumably registered previously via the use of `reg-fx`). - - When called with no args, it will unregister all currently registered effect handlers. - - When given one arg, assumed to be the `id` of a previously registered - effect handler, it will unregister the associated handler. Will produce a warning to - console if it finds no matching registration. - " - {:api-docs/hide true} - ([] - (registrar/clear-handlers fx/kind)) - ([id] - (registrar/clear-handlers fx/kind id))) - -;; -- coeffects --------------------------------------------------------------- - -(defn reg-cofx - "Register the given coeffect `handler` for the given `id`, for later use - within `inject-cofx`: - - - `id` is keyword, often namespaced. - - `handler` is a function which takes either one or two arguments, the first of which is - always `coeffects` and which returns an updated `coeffects`. - - See also: `inject-cofx` - " - {:api-docs/hide true} - [id handler] - (cofx/reg-cofx id handler)) - -(defn inject-cofx - "Given an `id`, and an optional, arbitrary `value`, returns an interceptor - whose `:before` adds to the `:coeffects` (map) by calling a pre-registered - 'coeffect handler' identified by the `id`. - - The previous association of a `coeffect handler` with an `id` will have - happened via a call to `re-frame.core/reg-cofx` - generally on program startup. - - Within the created interceptor, this 'looked up' `coeffect handler` will - be called (within the `:before`) with two arguments: - - - the current value of `:coeffects` - - optionally, the originally supplied arbitrary `value` - - This `coeffect handler` is expected to modify and return its first, `coeffects` argument. - - **Example of `inject-cofx` and `reg-cofx` working together** - - - First - Early in app startup, you register a `coeffect handler` for `:datetime`: - - #!clj - (re-frame.core/reg-cofx - :datetime ;; usage (inject-cofx :datetime) - (fn coeffect-handler - [coeffect] - (assoc coeffect :now (js/Date.)))) ;; modify and return first arg - - Second - Later, add an interceptor to an -fx event handler, using `inject-cofx`: - - #!clj - (re-frame.core/reg-event-fx ;; when registering an event handler - :event-id - [ ... (inject-cofx :datetime) ... ] ;; <-- create an injecting interceptor - (fn event-handler - [coeffect event] - ;;... in here can access (:now coeffect) to obtain current datetime ... - ))) - - **Background** - - `coeffects` are the input resources required by an event handler - to perform its job. The two most obvious ones are `db` and `event`. - But sometimes an event handler might need other resources. - - Perhaps an event handler needs a random number or a GUID or the current - datetime. Perhaps it needs access to a DataScript database connection. - - If an event handler directly accesses these resources, it stops being - pure and, consequently, it becomes harder to test, etc. So we don't - want that. - - Instead, the interceptor created by this function is a way to 'inject' - 'necessary resources' into the `:coeffects` (map) subsequently given - to the event handler at call time. - - See also `reg-cofx` - " - {:api-docs/hide true} - ([id] - (cofx/inject-cofx id)) - ([id value] - (cofx/inject-cofx id value))) - -(defn clear-cofx ;; think unreg-cofx - "Unregisters coeffect handlers (presumably registered previously via the use of `reg-cofx`). - - When called with no args, it will unregister all currently registered coeffect handlers. - - When given one arg, assumed to be the `id` of a previously registered - coeffect handler, it will unregister the associated handler. Will produce a warning to - console if it finds no matching registration." - {:api-docs/hide true} - ([] - (registrar/clear-handlers cofx/kind)) - ([id] - (registrar/clear-handlers cofx/kind id))) - -;; -- interceptors ------------------------------------------------------------ - -(def ^{:api-docs/hide true} debug - "An interceptor which logs/instruments an event handler's actions to - `re-frame/console` at the `:log` level. - - Output includes: - - 1. the event vector - 2. a `clojure.data/diff` of db, before vs after, which shows - the changes caused by the event handler. To understand the output, - you should understand: - https://clojuredocs.org/clojure.data/diff. - - You'd typically include this interceptor after (to the right of) any - `path` interceptor. - - Warning: calling `clojure.data/diff` on large, complex data structures - can be slow. So, you won't want this interceptor present in production - code. So, you should condition it out like this: - - #!clj - (re-frame.core/reg-event-db - :evt-id - [(when ^boolean goog.DEBUG re-frame.core/debug)] ;; <-- conditional - (fn [db v] - ...)) - - To make this code fragment work, you'll also have to set `goog.DEBUG` to - `false` in your production builds. For an example, look in `project.clj` of /examples/todomvc. - " - std-interceptors/debug) - -(defn path - "Returns an interceptor which acts somewhat like `clojure.core/update-in`, in the sense that - the event handler is given a specific part of `app-db` to change, not all of `app-db`. - - The interceptor has both a `:before` and `:after` functions. The `:before` replaces - the `:db` key within coeffects with a sub-path within `app-db`. The `:after` reverses the process, - and it grafts the handler's return value back into db, at the right path. - - Examples: - - #!clj - (path :some :path) - (path [:some :path]) - (path [:some :path] :to :here) - (path [:some :path] [:to] :here) - - Example Use: - - #!clj - (reg-event-db - :event-id - (path [:a :b]) ;; <-- used here, in interceptor chain - (fn [b v] ;; 1st arg is not db. Is the value from path [:a :b] within db - ... new-b)) ;; returns a new value for that path (not the entire db) - - Notes: - - 1. `path` may appear more than once in an interceptor chain. Progressive narrowing. - 2. if `:effects` contains no `:db` effect, can't graft a value back in. - " - {:api-docs/hide true} - [& args] - (apply std-interceptors/path args)) - -(defn enrich - "Returns an interceptor which will run the given function `f` in the `:after` - position. - - `f` is called with two arguments: `db` and `event`, and is expected to - return a modified `db`. - - Unlike the `after` interceptor which is only about side effects, `enrich` - expects `f` to process and alter the given `db` coeffect in some useful way, - contributing to the derived data, flowing vibe. - - If `f` returns `nil`, the `db` value passed to `f` will be returned instead. - - #### Example Use: - - Imagine that todomvc needed to do duplicate detection - if any two todos had - the same text, then highlight their background, and report them via a warning - at the bottom of the panel. - - Almost any user action (edit text, add new todo, remove a todo) requires a - complete reassessment of duplication errors and warnings. E.g. that edit - just made might have introduced a new duplicate, or removed one. Same with - any todo removal. So we need to re-calculate warnings after any CRUD events - associated with the todos list. - - Unless we are careful, we might end up coding subtly different checks - for each kind of CRUD operation. The duplicates check made after - 'delete todo' event might be subtly different to that done after an - editing operation. Nice and efficient, but fiddly. A bug generator - approach. - - So, instead, we create an `f` which recalculates ALL warnings from scratch - every time there is ANY change. It will inspect all the todos, and - reset ALL FLAGS every time (overwriting what was there previously) - and fully recalculate the list of duplicates (displayed at the bottom?). - - https://twitter.com/nathanmarz/status/879722740776939520 - - By applying `f` in an `:enrich` interceptor, after every CRUD event, - we keep the handlers simple and yet we ensure this important step - (of getting warnings right) is not missed on any change. - - We can test `f` easily - it is a pure function - independently of - any CRUD operation. - - This brings huge simplicity at the expense of some re-computation - each time. This may be a very satisfactory trade-off in many cases. - - #### Returning nil - - In some cases, it's useful to apply a change to specific situations that can - be determined at runtime instead of when defining the handler with an - `:enrich` interceptor. Instead of forcing you to return the `db` from every - non-applicable branch, you can return `nil` to use the given `db` value: - - #!clj - (def set-last-update - (core/enrich - (fn [{db :db} [_ {user :user}]] - (when (active-user? user) ;; <- Only perform an update if user is active - ...)))) - " - {:api-docs/hide true} - [f] - (std-interceptors/enrich f)) - -(def ^{:api-docs/hide true} unwrap - "> New in v1.2.0 - - An interceptor which decreases the amount of destructuring necessary in an - event handler where the event is structured as a 2-vector of - [event-id payload-map]. - - It promotes the `payload-map` part to be the event ultimately given to the - event handler. Should you want the full original event, it can be found in - `coeffects` under the key `:original-event`. - - If a dispatch looked like this: - - #!clj - (dispatch [:event-id {:x 1 :y 2 :z 3}]) - - Your event handlers can look like this: - - #!clj - (reg-event-fx - :event-id - [... unwrap ...] ;; <-- added to the interceptors - (fn [{:keys [db]} {:keys [x y z]}] ;; <-- instead of [_ {:keys [x y z]}] - ...) - " - std-interceptors/unwrap) - -(def ^{:api-docs/hide true} trim-v - "An interceptor which removes the first element of the event vector, - before it is supplied to the event handler, allowing you to write more - aesthetically pleasing event handlers. No leading underscore on the event-v! - - Should you want the full original event, it can be found in `coeffects` under - the key `:original-event`. - - Your event handlers will look like this: - - #!clj - (reg-event-db - :event-id - [... trim-v ...] ;; <-- added to the interceptors - (fn [db [x y z]] ;; <-- instead of [_ x y z] - ...) - " - std-interceptors/trim-v) - -(defn after - "Returns an interceptor which runs the given function `f` in the `:after` - position, presumably for side effects. - - `f` is called with two arguments: the `:effects` value for `:db` - (or the `:coeffect` value of `:db` if no `:db` effect is returned) and the event. - Its return value is ignored, so `f` can only side-effect. - - An example of use can be seen in the re-frame github repo in `/examples/todomvc/events.cljs`: - - - `f` runs schema validation (reporting any errors found). - - `f` writes to localstorage." - {:api-docs/hide true} - [f] - (std-interceptors/after f)) - -(defn on-changes - "Returns an interceptor which will observe N paths within `db`, and if any of them - test not `identical?` to their previous value (as a result of a event handler - being run), then it will run `f` to compute a new value, which is then assoc-ed - into the given `out-path` within `db`. - - Example Usage: - - #!clj - (defn my-f - [a-val b-val] - ... some computation on a and b in here) - - ;; use it - (def my-interceptor (on-changes my-f [:c] [:a] [:b])) - - (reg-event-db - :event-id - [... my-interceptor ...] ;; <-- ultimately used here - (fn [db v] - ...)) - - - If you put this interceptor on handlers which might change paths `:a` or `:b`, - it will: - - - call `f` each time the value at path `[:a]` or `[:b]` changes - - call `f` with the values extracted from `[:a]` `[:b]` - - assoc the return value from `f` into the path `[:c]` - " - {:api-docs/hide true} - [f out-path & in-paths] - (apply std-interceptors/on-changes f out-path in-paths)) - -(defn reg-global-interceptor - "Registers the given `interceptor` as a global interceptor. Global interceptors are - included in the processing chain of every event. - - When you register an event handler, you have the option of supplying an - interceptor chain. Any global interceptors you register are effectively - prepending to this chain. - - Global interceptors are run in the order that they are registered. - - Global interceptors are unique by :id. If a global interceptor with the same :id - key as `interceptor` is already registered, `interceptor` will take its place in the - global interceptor chain. This facilitates hot-reloading. - - Note: members of re-frame.std-interceptors do not have unique ids. To register - more than one, consider: - - (reg-global-interceptor (-> (re-frame.std-interceptors/on-changes + [:a] [:b]) - (assoc :id :my-unique-id)))" - {:api-docs/hide true} - [interceptor] - (settings/reg-global-interceptor interceptor)) - -(defn clear-global-interceptor - "Unregisters global interceptors (presumably registered previously via the use of `reg-global-interceptor`). - - When called with no args, it will unregister all currently registered global interceptors. - - When given one arg, assumed to be the `id` of a previously registered - global interceptors, it will unregister the associated interceptor. Will produce a warning to - console if it finds no matching registration." - {:api-docs/hide true} - ([] - (settings/clear-global-interceptors)) - ([id] - (settings/clear-global-interceptors id))) - -(defn ->interceptor - "A utility function for creating interceptors. - - Accepts three optional, named arguments: - - - `:id` - an id for the interceptor (decorative only) - - `:before` - the interceptor's before function - - `:after` - the interceptor's after function - - Example use: - - #!clj - (def my-interceptor - (->interceptor - :id :my-interceptor - :before (fn [context] - ... modifies and returns `context`) - :after (fn [context] - ... modifies and returns `context`))) - - Notes: - - - `:before` functions modify and return their `context` argument. Sometimes they - only side effect, in which case, they'll perform the side effect and return - `context` unchanged. - - `:before` functions often modify the `:coeffects` map within `context` and, - if they do, then they should use the utility functions `get-coeffect` and - `assoc-coeffect`. - - `:after` functions modify and return their `context` argument. Sometimes they - only side effect, in which case, they'll perform the side effect and return - `context` unchanged. - - `:after` functions often modify the `:effects` map within `context` and, - if they do, then they should use the utility functions `get-effect` - and `assoc-effect`" - {:api-docs/hide true} - [& {:as m :keys [id before after]}] - (utils/apply-kw interceptor/->interceptor m)) - -(defn get-coeffect - "A utility function, typically used when writing an interceptor's `:before` function. - - When called with one argument, it returns the `:coeffects` map from within that `context`. - - When called with two or three arguments, behaves like `clojure.core/get` and - returns the value mapped to `key` in the `:coeffects` map within `context`, `not-found` or - `nil` if `key` is not present." - {:api-docs/hide true} - ([context] - (interceptor/get-coeffect context)) - ([context key] - (interceptor/get-coeffect context key)) - ([context key not-found] - (interceptor/get-coeffect context key not-found))) - -(defn assoc-coeffect - "A utility function, typically used when writing an interceptor's `:before` function. - - Adds or updates a key/value pair in the `:coeffects` map within `context`. " - {:api-docs/hide true} - [context key value] - (interceptor/assoc-coeffect context key value)) - -(defn get-effect - "A utility function, used when writing interceptors, typically within an `:after` function. - - When called with one argument, returns the `:effects` map from the `context`. - - When called with two or three arguments, behaves like `clojure.core/get` and - returns the value mapped to `key` in the effects map, `not-found` or - `nil` if `key` is not present." - {:api-docs/hide true} - ([context] - (interceptor/get-effect context)) - ([context key] - (interceptor/get-effect context key)) - ([context key not-found] - (interceptor/get-effect context key not-found))) - -(defn assoc-effect - "A utility function, typically used when writing an interceptor's `:after` function. - - Adds or updates a key/value pair in the `:effects` map within `context`. " - {:api-docs/hide true} - [context key value] - (interceptor/assoc-effect context key value)) - -(defn enqueue - "A utility function, used when writing an interceptor's `:before` function. - - Adds the given collection of `interceptors` to those already in `context's` - execution `:queue`. It returns the updated `context`. - - So, it provides a way for one interceptor to add more interceptors to the - currently executing interceptor chain. - " - {:api-docs/hide true} - [context interceptors] - (interceptor/enqueue context interceptors)) - -;; -- logging ---------------------------------------------------------------- - -(defn set-loggers! - "re-frame outputs warnings and errors via the API function `console` - which, by default, delegates to `js/console`'s default implementation for - `log`, `error`, `warn`, `debug`, `group` and `groupEnd`. But, using this function, - you can override that behaviour with your own functions. - - The argument `new-loggers` should be a map containing a subset of they keys - for the standard `loggers`, namely `:log` `:error` `:warn` `:debug` `:group` - or `:groupEnd`. - - Example Usage: - - #!clj - (defn my-logger ;; my alternative logging function - [& args] - (post-it-somewhere (apply str args))) - - ;; now install my alternative loggers - (re-frame.core/set-loggers! {:warn my-logger :log my-logger}) - " - {:api-docs/hide true} - [new-loggers] - (loggers/set-loggers! new-loggers)) - -(defn console - "A utility logging function which is used internally within re-frame to produce - warnings and other output. It can also be used by libraries which - extend re-frame, such as effect handlers. - - By default, it will output the given `args` to `js/console` at the given log `level`. - However, an application using re-frame can redirect `console` output via `set-loggers!`. - - `level` can be one of `:log`, `:error`, `:warn`, `:debug`, `:group` or `:groupEnd`. - - Example usage: - - #!clj - (console :error \"Sure enough it happened:\" a-var \"and\" another) - (console :warn \"Possible breach of containment wall at:\" dt) - " - {:api-docs/hide true} - [level & args] - (apply loggers/console level args)) - -;; -- unit testing ------------------------------------------------------------ - -(defn make-restore-fn - "This is a utility function, typically used in testing. - - It checkpoints the current state of re-frame and returns a function which, when - later called, will restore re-frame to the checkpointed state. - - The checkpoint includes `app-db`, all registered handlers and all subscriptions. - " - {:api-docs/hide true} - [] - (let [handlers @registrar/kind->id->handler - app-db @db/app-db - subs-cache @subs/query->reaction] - (fn [] - ;; call `dispose!` on all current subscriptions which - ;; didn't originally exist. - (let [original-subs (set (vals subs-cache)) - current-subs (set (vals @subs/query->reaction))] - (doseq [sub (set/difference current-subs original-subs)] - (interop/dispose! sub))) - - ;; Reset the atoms - ;; We don't need to reset subs/query->reaction, as - ;; disposing of the subs removes them from the cache anyway - (reset! registrar/kind->id->handler handlers) - (reset! db/app-db app-db) - nil))) - -(defn purge-event-queue - "Removes all events currently queued for processing" - {:api-docs/hide true} - [] - (router/purge re-frame.router/event-queue)) - -;; -- Event Processing Callbacks --------------------------------------------- - -(defn add-post-event-callback - "Registers the given function `f` to be called after each event is processed. - - `f` will be called with two arguments: - - - `event`: a vector. The event just processed. - - `queue`: a PersistentQueue, possibly empty, of events yet to be processed. - - This facility is useful in advanced cases like: - - - you are implementing a complex bootstrap pipeline - - you want to create your own handling infrastructure, with perhaps multiple - handlers for the one event, etc. Hook in here. - - libraries providing 'isomorphic javascript' rendering on Nodejs or Nashorn. - - `id` is typically a keyword. If it supplied when an `f` is added, it can be - subsequently be used to identify it for removal. See `remove-post-event-callback`. - " - {:api-docs/hide true} - ([f] - (add-post-event-callback f f)) ;; use f as its own identifier - ([id f] - (router/add-post-event-callback re-frame.router/event-queue id f))) - -(defn remove-post-event-callback - "Unregisters a post event callback function, identified by `id`. - - Such a function must have been previously registered via `add-post-event-callback`" - {:api-docs/hide true} - [id] - (router/remove-post-event-callback re-frame.router/event-queue id)) - -;; -- Flows ------------------------------------------------------------------ - -(defn reg-flow - "Registers a `flow`. - - A full tutorial can be found at https://day8.github.io/re-frame/Flows - - Re-frame uses the flow registry to execute a dataflow graph. - - On every event, re-frame runs each registered `flow`. - It resolves the flow's inputs, determines if the flow is live, and if so, - evaluates the output function, putting the result in `app-db` at the path. - - A `flow` is a map, specifying one dataflow node. It has keys: - - - `:id`: uniquely identifies the node. - - When a `flow` is already registered with the same `:id`, replaces it. - - You can provide an `id` argument to `reg-flow`, instead of including `:id`. - - `:inputs`: a map of `keyword->input`. An input can be one of two types: - - vector: expresses a path in `app-db`. - - map: expresses the output of another flow, identified by a - `::re-frame.flow.alpha/flow<-` key. - Call the `re-frame.alpha/flow<-` function to construct this map. - - `:output`: a function of the `keyword->resolved-input` map, returning the output value of the node. - - A resolved vector input is the value in `app-db` at that path. - - A resolved `flow<-` input is the value in `app-db` at the path of the named flow. - - Re-frame topologically sorts the flows, to make sure any input flows always run first. - - Re-frame throws an error at registration time if any flow inputs form a cycle. - - `:path`: specifies the `app-db` location where the `:output` value is stored. - - `:live-inputs`: a map of `keyword->live-input` for the `:live?` function. - - A `live-input` works the same way an `input`. - - `:live?`: a predicate function of the `keyword->resolved-live-input` map, returning the current lifecycle state of the node. - - `:cleanup`: a function of `app-db` and the `:path`. - - Returns a new `app-db`. - - Runs the first time `:live?` returns `false` - - Runs when the flow is cleared (see `re-frame.alpha/clear-flow`). - - `:id` is the only required key. All others have a default value: - - - `:path`: `[id]` - - `:inputs`: `{}` - - `:output`: `(constantly true)` - - `:live?`: `(constantly true)` - - `:live-inputs`: `{}` - - `:cleanup`: `re-frame.utils/dissoc-in`" - {:api-docs/heading "Flows"} - ([flow] (flow/reg-flow flow)) - ([id flow] (flow/reg-flow id flow))) - -(defn clear-flow - "Arguments: `[id]` - Deregisters a flow, identified by `id`. - - Later, re-frame will update `app-db` with the flow's `:cleanup` function. - - If `clear-flow` is invoked by the `:clear-flow` effect, this cleanup happens in the `:after` phase of the same event returning `:clear-flow`. - - If you call `clear-flow` directly, cleanup will happen on the next event." - {:api-docs/heading "Flows"} - ([] (flow/clear-flow)) - ([id] (flow/clear-flow id))) - -(defn get-flow - "Returns the value within `db` at the `:path` given by the registered flow - with an `:id` key equal to `id`, if it exists. Otherwise, returns nil." - {:api-docs/heading "Flows"} - [db id] (flow/resolve-input db (flow/flow<- id))) - -(defn flow<- - "Creates an input from a flow id." - {:api-docs/heading "Flows"} - [id] - (flow/flow<- id)) - -(reg :sub :flow - (fn [db input] - (flow/resolve-input db input))) - -(reg :sub :live? - (fn [db input] - (flow/resolve-input db input))) - -(reg-fx :reg-flow flow/reg-flow) - -(reg-fx :clear-flow flow/clear-flow) - -;; -- Deprecation ------------------------------------------------------------ - -(def ^{:api-docs/heading "Legacy Compatibility"} subscribe - "Equivalent to `sub` (except with flows, which have their own lifecycle and are not cached). - - Call `(subscribe [:flow {:id :your-flow-id}])` to subscribe to a flow." - sub) - -(defn reg-sub - "Equivalent to `reg` `:legacy-sub`." - {:api-docs/heading "Legacy Compatibility"} - [& args] - (apply reg :legacy-sub args)) - -;; Assisting the v0.7.x -> v0.8.x transition. -(defn register-handler - "Deprecated. Use `reg-event-db` instead." - {:deprecated "0.8.0" - :api-docs/hide true} - [& args] - (console :warn "re-frame: \"register-handler\" has been renamed \"reg-event-db\" (look for registration of " (str (first args)) ")") - (apply reg-event-db args)) - -(defn register-sub - "Deprecated. Use `reg-sub-raw` instead." - {:deprecated "0.8.0" - :api-docs/hide true} - [& args] - (console :warn "re-frame: \"register-sub\" is used to register the event " (str (first args)) " but it is a deprecated part of the API. Please use \"reg-sub-raw\" instead.") - (apply reg-sub-raw args)) diff --git a/src/re_frame/cofx.cljc b/src/re_frame/cofx.cljc index 49d0eb60..6754b7e6 100644 --- a/src/re_frame/cofx.cljc +++ b/src/re_frame/cofx.cljc @@ -1,8 +1,7 @@ (ns re-frame.cofx (:require - [re-frame.db :refer [app-db]] [re-frame.interceptor :refer [->interceptor]] - [re-frame.registrar :refer [get-handler register-handler]] + [re-frame.registrar :refer [get-handler register-handler *current-frame*]] [re-frame.loggers :refer [console]])) ;; -- Registration ------------------------------------------------------------ @@ -10,42 +9,38 @@ (def kind :cofx) (assert (re-frame.registrar/kinds kind)) -(defn reg-cofx - [id handler] - (register-handler kind id handler)) - ;; -- Interceptor ------------------------------------------------------------- (defn inject-cofx - ([id] + ([registry id] (->interceptor :id :coeffects :before (fn coeffects-before [context] - (if-let [handler (get-handler kind id)] - (update context :coeffects handler) + (if-let [handler (get-handler registry kind id)] + (binding [*current-frame* (:frame context)] + (update context :coeffects handler)) (console :error "No cofx handler registered for" id))))) - ([id value] + ([registry id value] (->interceptor :id :coeffects :before (fn coeffects-before [context] - (if-let [handler (get-handler kind id)] - (update context :coeffects handler value) + (if-let [handler (get-handler registry kind id)] + (binding [*current-frame* (:frame context)] + (update context :coeffects handler value)) (console :error "No cofx handler registered for" id)))))) ;; -- Builtin CoEffects Handlers --------------------------------------------- -;; :db -;; -;; Adds to coeffects the value in `app-db`, under the key `:db` -(reg-cofx - :db - (fn db-coeffects-handler - [coeffects] - (assoc coeffects :db @app-db))) - -;; Because this interceptor is used so much, we reify it -(def inject-db (inject-cofx :db)) - - +(defn register-built-in! + [{:keys [registry]}] + (let [reg-cofx (partial register-handler registry kind)] + ;; :db + ;; + ;; Adds to coeffects the value in `app-db`, under the key `:db` + (reg-cofx + :db + (fn db-coeffects-handler + [coeffects] + (assoc coeffects :db @(:app-db *current-frame*)))))) diff --git a/src/re_frame/context.clj b/src/re_frame/context.clj new file mode 100644 index 00000000..8e6939bf --- /dev/null +++ b/src/re_frame/context.clj @@ -0,0 +1,42 @@ +(ns re-frame.context + (:refer-clojure :exclude [bound-fn]) + (:require [cljs.env] + [cljs.analyzer])) + +(defmacro defc + "For definining Reagent components that honor the contextual frame. Like defn + but sets a :context-type metadata on the function, which Reagent will pick up + on, so that the correct React context is set for this component." + [name & fntail] + (let [[doc fntail] (if (string? (first fntail)) + [(first fntail) (rest fntail)] + [nil fntail])] + `(def ~(with-meta name (merge {:doc doc} (:meta &form))) + ^{:context-type frame-context} + (fn ~@fntail)))) + +(defmacro bind-frame [frame & body] + `(binding [~'re-frame.registrar/*current-frame* ~frame] + (assert (satisfies? ~'re-frame.frame/IFrame ~frame) "given frame is not of type `re-frame.frame/IFrame`") + ~@body)) + +(defmacro import-with-frame + ([var-sym] + `(import-with-frame ~(symbol (name var-sym)) ~var-sym)) + ([name var-sym] + `(defn ~name + ;; Attempt at propagating the doc string / arglists, for some reason CIDER + ;; is not picking this up though. + ~(select-keys (:meta (cljs.analyzer/resolve-var cljs.env/*compiler* var-sym)) + [:doc :arglists]) + [& args#] + (apply ~var-sym (current-frame) args#)))) + +(defmacro bound-fn [& args] + (let [[name argv & body] (if (symbol? (first args)) + args + (into [nil] args))] + `(let [frame# (~'re-frame.context/current-frame)] + (fn ~@(when name name) ~argv + (binding [~'re-frame.registrar/*current-frame* frame#] + ~@body))))) diff --git a/src/re_frame/context.cljs b/src/re_frame/context.cljs new file mode 100644 index 00000000..a09bae4b --- /dev/null +++ b/src/re_frame/context.cljs @@ -0,0 +1,110 @@ +(ns re-frame.context + (:require ["react" :as react] + [goog.object :as gobj] + [re-frame.core :as r] + [re-frame.frame :as frame] + [re-frame.registrar :as registrar] + [re-frame.subs :as subs] + [reagent.core]) + (:require-macros [re-frame.context :refer [defc import-with-frame]])) + +(def frame-context (.createContext react r/default-frame)) + +(defn set-default-frame [frame] + (gobj/set frame-context "_currentValue" frame) + (gobj/set frame-context "_currentValue2" frame)) + +(defn current-context + "Gets the react Context for the current component, to be used in lifecycle + hooks (e.g. render). Assumes that Component.contextType has been set." + [] + (when-let [cmp (reagent.core/current-component)] + ;; When used without setting the right contextType we will get #js {} back + (when (not (object? (.-context cmp))) + (.-context cmp)))) + +(defn current-frame + "Get the current frame provided by the context, falling back to the default + frame. Assumes that Component.contextType = frame-context." + [] + (or registrar/*current-frame* + (current-context) + (gobj/get frame-context "_currentValue"))) + +(defn bound-frame [] + (or registrar/*current-frame* + (current-context) + (throw (js/Error. "No frame bound")))) + +(defn provide-frame + "Component that acts as a provider for the frame, so to run an isolated version + of your app, use. + + [provide-frame (frame/make-frame) + [app]]" + [frame & children] + (reagent.core/create-element + (.-Provider frame-context) + #js {:value frame + :children (reagent.core/as-element (into [:<>] children))})) + +(defc provide-app-db + "Component that acts as a provider for the app-db, it takes the registry from + the current frame, but uses the given atom for the app-db" + [app-db & children] + `[~provide-frame ~(frame/make-frame {:registry (:registry (current-frame)) + :app-db app-db}) + ~@children]) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Complete copy of the top-level re-frame API. If you are using the context +;; approach then import re-frame.context instead of re-frame.core and things +;; should generally Just Work™ + +(import-with-frame subscribe re-frame.frame/subscribe) +(import-with-frame dispatch re-frame.frame/dispatch) +(import-with-frame dispatch-sync re-frame.frame/dispatch-sync) +(import-with-frame clear-sub re-frame.frame/clear-sub) +(import-with-frame reg-fx re-frame.frame/reg-fx) +(import-with-frame reg-cofx re-frame.frame/reg-cofx) +(import-with-frame inject-cofx re-frame.frame/inject-cofx) +(import-with-frame clear-cofx re-frame.frame/clear-cofx) +(import-with-frame reg-event-db re-frame.frame/reg-event-db) +(import-with-frame reg-event-fx re-frame.frame/reg-event-fx) +(import-with-frame reg-event-ctx re-frame.frame/reg-event-ctx) +(import-with-frame clear-event re-frame.frame/clear-event) + +;; A few special cases which we can't import directly + +(defn reg-sub-raw [query-id handler-fn] + (frame/reg-sub-raw + (current-frame) + query-id + (fn [frame query-v] + (handler-fn (:app-db frame) query-v)))) + +(defn reg-sub [query-id & args] + (apply frame/reg-sub (current-frame) query-id args)) + +(defn clear-subscriptions-cache! [& args] + (apply subs/-clear (:subs-cache (current-frame)) args)) + + +(defn context-fns + "Returns subscribe/dispatch/dispatch-sync functions that are bound to the current frame. Use like this + + (defc my-component [] + (reagent/with-let [{:keys [subscribe dispatch]} (re-frame/context-fns)] + ,,, + )) " + ([] (context-fns (current-frame))) + ([frame] + {:subscribe (partial re-frame.frame/subscribe frame) + :dispatch (partial re-frame.frame/dispatch frame) + :dispatch-sync (partial re-frame.frame/dispatch-sync frame)})) + +(defn bind-fn [f] + (let [frame (current-frame)] + (fn [& args] + (binding [registrar/*current-frame* frame] + (apply f args))))) diff --git a/src/re_frame/core.cljc b/src/re_frame/core.cljc index 8ae500e4..1acbcb3e 100644 --- a/src/re_frame/core.cljc +++ b/src/re_frame/core.cljc @@ -1,21 +1,13 @@ (ns re-frame.core (:require - [re-frame.events :as events] - [re-frame.subs :as subs] - [re-frame.interop :as interop] - [re-frame.db :as db] - [re-frame.fx :as fx] - [re-frame.cofx :as cofx] + [re-frame.frame :as frame] + [re-frame.db :refer [default-frame]] [re-frame.router :as router] [re-frame.settings :as settings] [re-frame.loggers :as loggers] - [re-frame.registrar :as registrar] [re-frame.interceptor :as interceptor] - [re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor - fx-handler->interceptor - ctx-handler->interceptor]] - [re-frame.utils :as utils] - [clojure.set :as set])) + [re-frame.std-interceptors :as std-interceptors] + [re-frame.utils :as utils])) ;; -- dispatch ---------------------------------------------------------------- @@ -37,7 +29,7 @@ " {:api-docs/heading "Dispatching Events"} [event] - (router/dispatch event)) + (frame/dispatch default-frame event)) (defn dispatch-sync "Synchronously (immediately) process `event`. It does **not** queue @@ -65,7 +57,7 @@ " {:api-docs/heading "Dispatching Events"} [event] - (router/dispatch-sync event)) + (frame/dispatch-sync default-frame event)) ;; -- Events ------------------------------------------------------------------ @@ -98,9 +90,9 @@ " {:api-docs/heading "Event Handlers"} ([id handler] - (reg-event-db id nil handler)) + (frame/reg-event-db default-frame id handler)) ([id interceptors handler] - (events/register id [cofx/inject-db fx/do-fx std-interceptors/inject-global-interceptors interceptors (db-handler->interceptor handler)]))) + (frame/reg-event-db default-frame id interceptors handler))) (defn reg-event-fx "Register the given event `handler` (function) for the given `id`. Optionally, provide @@ -132,9 +124,9 @@ " {:api-docs/heading "Event Handlers"} ([id handler] - (reg-event-fx id nil handler)) + (frame/reg-event-fx default-frame id handler)) ([id interceptors handler] - (events/register id [cofx/inject-db fx/do-fx std-interceptors/inject-global-interceptors interceptors (fx-handler->interceptor handler)]))) + (frame/reg-event-fx default-frame id interceptors handler))) (defn reg-event-ctx "Register the given event `handler` (function) for the given `id`. Optionally, provide @@ -163,9 +155,9 @@ " {:api-docs/heading "Event Handlers"} ([id handler] - (reg-event-ctx id nil handler)) + (frame/reg-event-ctx default-frame id nil handler)) ([id interceptors handler] - (events/register id [cofx/inject-db fx/do-fx std-interceptors/inject-global-interceptors interceptors (ctx-handler->interceptor handler)]))) + (frame/reg-event-ctx default-frame id interceptors handler))) (defn clear-event "Unregisters event handlers (presumably registered previously via the use of `reg-event-db` or `reg-event-fx`). @@ -177,9 +169,9 @@ console if it finds no matching registration." {:api-docs/heading "Event Handlers"} ([] - (registrar/clear-handlers events/kind)) + (frame/clear-event default-frame)) ([id] - (registrar/clear-handlers events/kind id))) + (frame/clear-event default-frame id))) ;; -- subscriptions ----------------------------------------------------------- @@ -432,7 +424,7 @@ " {:api-docs/heading "Subscriptions"} [query-id & args] - (apply subs/reg-sub query-id args)) + (apply frame/reg-sub default-frame query-id args)) (defn subscribe "Given a `query` vector, returns a Reagent `reaction` which will, over @@ -496,9 +488,9 @@ " {:api-docs/heading "Subscriptions"} ([query] - (subs/subscribe query)) + (frame/subscribe default-frame query)) ([query dynv] - (subs/subscribe query dynv))) + (frame/subscribe default-frame query dynv))) (defn clear-sub ;; think unreg-sub "Unregisters subscription handlers (presumably registered previously via the use of `reg-sub`). @@ -512,9 +504,9 @@ NOTE: Depending on the usecase, it may be necessary to call `clear-subscription-cache!` afterwards" {:api-docs/heading "Subscriptions"} ([] - (registrar/clear-handlers subs/kind)) + (frame/clear-sub default-frame)) ([query-id] - (registrar/clear-handlers subs/kind query-id))) + (frame/clear-sub default-frame query-id))) (defn reg-sub-raw "This is a low level, advanced function. You should probably be @@ -524,7 +516,7 @@ http://day8.github.io/re-frame/flow-mechanics/" {:api-docs/heading "Subscriptions"} [query-id handler-fn] - (registrar/register-handler subs/kind query-id handler-fn)) + (frame/reg-sub-raw default-frame query-id handler-fn)) ;; XXX (defn clear-subscription-cache! @@ -538,7 +530,7 @@ " {:api-docs/heading "Subscriptions"} [] - (subs/clear-subscription-cache!)) + (frame/clear-subscriptions-cache default-frame)) ;; -- effects ----------------------------------------------------------------- @@ -567,7 +559,7 @@ " {:api-docs/heading "Effect Handlers"} [id handler] - (fx/reg-fx id handler)) + (frame/reg-fx default-frame id handler)) (defn clear-fx ;; think unreg-fx "Unregisters effect handlers (presumably registered previously via the use of `reg-fx`). @@ -580,9 +572,9 @@ " {:api-docs/heading "Effect Handlers"} ([] - (registrar/clear-handlers fx/kind)) + (frame/clear-fx default-frame)) ([id] - (registrar/clear-handlers fx/kind id))) + (frame/clear-fx default-frame id))) ;; -- coeffects --------------------------------------------------------------- @@ -598,7 +590,7 @@ " {:api-docs/heading "Coeffects"} [id handler] - (cofx/reg-cofx id handler)) + (frame/reg-cofx default-frame id handler)) (defn inject-cofx "Given an `id`, and an optional, arbitrary `value`, returns an interceptor @@ -660,9 +652,9 @@ " {:api-docs/heading "Coeffects"} ([id] - (cofx/inject-cofx id)) + (frame/inject-cofx default-frame id)) ([id value] - (cofx/inject-cofx id value))) + (frame/inject-cofx default-frame id value))) (defn clear-cofx ;; think unreg-cofx "Unregisters coeffect handlers (presumably registered previously via the use of `reg-cofx`). @@ -674,9 +666,9 @@ console if it finds no matching registration." {:api-docs/heading "Coeffects"} ([] - (registrar/clear-handlers cofx/kind)) + (frame/clear-cofx default-frame)) ([id] - (registrar/clear-handlers cofx/kind id))) + (frame/clear-cofx default-frame id))) ;; -- error handler ---------------------------------------------------------- @@ -705,7 +697,7 @@ - `:direction`: `:before` or `:after`. - `:event-v`: the re-frame event which invoked this interceptor." [handler] - (registrar/register-handler :error :event-handler handler)) + (frame/event-error-handler default-frame handler)) (reg-event-error-handler interceptor/default-error-handler) @@ -1132,29 +1124,13 @@ " {:api-docs/heading "Miscellaneous"} [] - (let [handlers @registrar/kind->id->handler - app-db @db/app-db - subs-cache @subs/query->reaction] - (fn [] - ;; call `dispose!` on all current subscriptions which - ;; didn't originally exist. - (let [original-subs (set (vals subs-cache)) - current-subs (set (vals @subs/query->reaction))] - (doseq [sub (set/difference current-subs original-subs)] - (interop/dispose! sub))) - - ;; Reset the atoms - ;; We don't need to reset subs/query->reaction, as - ;; disposing of the subs removes them from the cache anyway - (reset! registrar/kind->id->handler handlers) - (reset! db/app-db app-db) - nil))) + (frame/make-restore-fn default-frame)) (defn purge-event-queue "Removes all events currently queued for processing" {:api-docs/heading "Miscellaneous"} [] - (router/purge re-frame.router/event-queue)) + (router/purge (:event-queue default-frame))) ;; -- Event Processing Callbacks --------------------------------------------- @@ -1180,7 +1156,7 @@ ([f] (add-post-event-callback f f)) ;; use f as its own identifier ([id f] - (router/add-post-event-callback re-frame.router/event-queue id f))) + (router/add-post-event-callback (:event-queue default-frame) id f))) (defn remove-post-event-callback "Unregisters a post event callback function, identified by `id`. @@ -1188,7 +1164,7 @@ Such a function must have been previously registered via `add-post-event-callback`" {:api-docs/heading "Miscellaneous"} [id] - (router/remove-post-event-callback re-frame.router/event-queue id)) + (router/remove-post-event-callback (:event-queue default-frame) id)) ;; -- Deprecation ------------------------------------------------------------ ;; Assisting the v0.7.x -> v0.8.x transition. diff --git a/src/re_frame/db.cljc b/src/re_frame/db.cljc index 5ee49bd5..1c2d33e3 100644 --- a/src/re_frame/db.cljc +++ b/src/re_frame/db.cljc @@ -1,10 +1,18 @@ (ns re-frame.db - (:require [re-frame.interop :refer [ratom]])) + (:require [re-frame.interop :refer [ratom]] + [re-frame.frame :as frame] + [re-frame.fx :as fx] + [re-frame.cofx :as cofx])) + + +(def default-frame (frame/make-frame)) + +(fx/register-built-in! default-frame) +(cofx/register-built-in! default-frame) ;; -- Application State -------------------------------------------------------------------------- ;; ;; Should not be accessed directly by application code. ;; Read access goes through subscriptions. ;; Updates via event handlers. -(def app-db (ratom {})) - +(def app-db (:app-db default-frame)) diff --git a/src/re_frame/events.cljc b/src/re_frame/events.cljc index 95bf8208..b03fedc5 100644 --- a/src/re_frame/events.cljc +++ b/src/re_frame/events.cljc @@ -1,6 +1,5 @@ (ns re-frame.events - (:require [re-frame.db :refer [app-db]] - [re-frame.utils :refer [first-in-vector]] + (:require [re-frame.utils :refer [first-in-vector]] [re-frame.interop :refer [empty-queue debug-enabled?]] [re-frame.registrar :refer [get-handler register-handler]] [re-frame.loggers :refer [console]] @@ -39,8 +38,8 @@ Typically, an `event handler` will be at the end of the chain (wrapped in an interceptor)." - [id interceptors] - (register-handler kind id (flatten-and-remove-nils id interceptors))) + [registry id interceptors] + (register-handler registry kind id (flatten-and-remove-nils id interceptors))) ;; -- handle event -------------------------------------------------------------------------------- @@ -48,9 +47,9 @@ (defn handle "Given an event vector `event-v`, look up the associated interceptor chain, and execute it." - [event-v] + [{:keys [registry app-db] :as frame} event-v] (let [event-id (first-in-vector event-v)] - (if-let [interceptors (get-handler kind event-id true)] + (if-let [interceptors (get-handler registry kind event-id true)] (if *handling* (console :error "re-frame: while handling" *handling* ", dispatch-sync was called for" event-v ". You can't call dispatch-sync within an event handler.") (binding [*handling* event-v] @@ -58,7 +57,5 @@ :op-type kind :tags {:event event-v}} (trace/merge-trace! {:tags {:app-db-before @app-db}}) - (interceptor/execute event-v interceptors) + (interceptor/execute frame event-v interceptors) (trace/merge-trace! {:tags {:app-db-after @app-db}}))))))) - - diff --git a/src/re_frame/flow/alpha.cljc b/src/re_frame/flow/alpha.cljc deleted file mode 100644 index 2f704fdd..00000000 --- a/src/re_frame/flow/alpha.cljc +++ /dev/null @@ -1,172 +0,0 @@ -(ns re-frame.flow.alpha - (:require - #?(:cljs [re-frame.db :as db]) - [re-frame.utils :as u] - [re-frame.registrar :refer [get-handler]] - [re-frame.loggers :refer [console]] - [re-frame.interceptor :refer [->interceptor get-effect get-coeffect assoc-effect]] - [re-frame.interop :as interop] - #?(:cljs [reagent.core :as r]))) - -(def db-path? vector?) - -(def flow? map?) - -(def flow<-? (comp some? ::flow<-)) - -(def flows (interop/ratom {})) - -(defn lookup [id] (get @flows id)) - -(defn input-ids [{:keys [inputs live-inputs]}] - (vec (distinct (into [] - (comp (remove db-path?) - (map #(or (::flow<- %) %))) - (concat (vals inputs) (vals live-inputs)))))) - -(defn topsort [flows] - (->> flows - (u/map-vals input-ids) - u/remove-orphans - u/topsort-kahn - reverse - (map flows))) - -(defn default [id] - {:id id - :path [id] - :inputs {} - :output (constantly true) - :live? (constantly true) - :live-inputs {} - :cleanup u/deep-dissoc}) - -(defn stale-in-flows [flows {:keys [inputs]}] - (reduce-kv (fn [m k {:keys [path]}] - (cond-> m - (contains? (set (vals inputs)) path) (assoc k path))) - {} - flows)) - -(defn stale-out-flows [flows {:keys [path]}] - (reduce-kv (fn [m k {:keys [inputs]}] - (let [bad-inputs (into {} (filter (comp #{path} val)) inputs)] - (cond-> m (seq bad-inputs) (assoc k bad-inputs)))) - {} - flows)) - -(defn validate-inputs [{:keys [inputs]}] - (doseq [[_ input] inputs - :when (not ((some-fn db-path? flow<-?) input))] - (throw (#?(:clj Exception. :cljs js/Error.) "bad input")))) - -(defn warn-stale-dependencies [flows new-flow] - (let [ins (stale-in-flows flows new-flow) - outs (stale-out-flows flows new-flow) - warn-ins (fn [[id path]] - ["- Input" (str path) - "matches the output path of" (str id) ".\n" - " For an explicit dependency, change it to (re-frame/flow<-" - (str id ").") "\n"]) - warn-outs (fn [[id inputs]] - (mapcat (fn [[input-id _]] - ["- Output" (str (:path new-flow)) - "matches the input" (str input-id) - "of the flow" (str id ".\n") - " For an explicit dependency, change that input to" - "(re-frame/flow<-" (str (:id new-flow) ").") "\n"]) - inputs)) - warnings (concat (mapcat warn-ins ins) (mapcat warn-outs outs))] - (when (seq warnings) - (apply console :warn "Warning: You called `reg-flow` with the flow" (str (:id new-flow)) - "but this created stale dependencies.\n" - "Your flows may not evaluate in the correct order.\n" - warnings)))) - -(defn reg-flow - ([k m] - (reg-flow (assoc m :id k))) - ([m] - (validate-inputs m) - (warn-stale-dependencies @flows m) - (swap! flows assoc - (:id m) (with-meta (merge (default (:id m)) m) - (merge - {::new? true} - #?(:cljs - {::ref (r/reaction (get-in @db/app-db (:path m)))})))))) - -(defn clear-flow - ([] - (swap! flows vary-meta update ::cleared into @flows) - (swap! flows empty)) - ([id] - (when-let [flow (lookup id)] - (swap! flows dissoc id) - (swap! flows vary-meta update ::cleared assoc (:id flow) flow)))) - -(defn flow<- [id] {::flow<- id}) - -(def flow-fx-ids #{:reg-flow :clear-flow}) - -(defn do-effect [[k v]] ((get-handler :fx k false) v)) - -(def remove-fx (partial remove flow-fx-ids)) - -(def do-fx - (->interceptor - {:id :do-flow-fx - :after (fn [{{:keys [fx] :as effects} :effects - :as ctx}] - (let [flow-fx (concat (select-keys effects flow-fx-ids) - (filterv (comp flow-fx-ids first) fx))] - (doall (map do-effect flow-fx)) - (-> ctx - (update-in [:effects :fx] remove-fx) - (update :effects (comp (partial into {}) remove-fx)))))})) - -(defn resolve-input [db input] - (if (vector? input) - (get-in db input) - (some->> input ::flow<- lookup :path (resolve-input db)))) - -(defn resolve-inputs [db inputs] - (if (empty? inputs) db (u/map-vals (partial resolve-input db) inputs))) - -(defn run [ctx {:as flow - :keys [path cleanup live? inputs live-inputs output id] - ::keys [cleared?]}] - (let [{::keys [new?]} (meta flow) - old-db (get-coeffect ctx :db) - db (or (get-effect ctx :db) old-db) - - id->old-in (resolve-inputs old-db inputs) - id->in (resolve-inputs db inputs) - dirty? (not= id->in id->old-in) - - id->old-live-in (resolve-inputs old-db live-inputs) - id->live-in (resolve-inputs db live-inputs) - bardo [(cond new? :new (live? id->old-live-in) :live :else :dead) - (cond cleared? :cleared (live? id->live-in) :live :else :dead)] - - new-db (case bardo - [:live :live] (cond-> db dirty? (assoc-in path (output id->in))) - [:live :dead] (cleanup db path) - [:dead :live] (assoc-in db path (output id->in)) - [:new :live] (do (swap! flows update id vary-meta dissoc ::new?) - (assoc-in db path (output id->in))) - [:live :cleared] (cleanup db path) - nil)] - (cond-> ctx new-db (assoc-effect :db new-db)))) - -(defn with-cleared [m] - (into m (map (fn [[k v]] [[::cleared k (gensym)] (assoc v ::cleared? true)]) - (::cleared (meta m))))) - -(def interceptor - (->interceptor - {:id :flow - :after (fn [ctx] - (let [all-flows (with-cleared @flows)] - (swap! flows vary-meta dissoc ::cleared) - (reduce run ctx ((memoize topsort) all-flows))))})) diff --git a/src/re_frame/frame.cljc b/src/re_frame/frame.cljc new file mode 100644 index 00000000..66ad31a7 --- /dev/null +++ b/src/re_frame/frame.cljc @@ -0,0 +1,236 @@ +(ns re-frame.frame + (:require [re-frame.cofx :as cofx] + [re-frame.events :as events] + [re-frame.fx :as fx] + [re-frame.interop :as interop] + [re-frame.registrar :as reg] + [re-frame.router :as router] + [re-frame.std-interceptors :as stdi] + [re-frame.subs :as subs])) + +(defprotocol IFrame + ;; dispatch ---- + (dispatch [this event-v]) + (dispatch-sync [this event-v]) + + ;; subs ---- + (reg-sub-raw [this query-id handler-fn]) + (reg-sub + ;; variadic args are not supported in protocols + [this query-id arg1] + [this query-id arg1 arg2] + [this query-id arg1 arg2 arg3] + [this query-id arg1 arg2 arg3 arg4] + [this query-id arg1 arg2 arg3 arg4 arg5] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15] + [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16]) + + (subscribe + [this query-v] + [this query-v dynv]) + (clear-sub + [this] + [this query-id]) + (clear-subscriptions-cache [this]) + + ;; fx ---- + (reg-fx [this fx-id handler-fn]) + (clear-fx + [this] + [this fx-id]) + + ;; cofx ---- + (reg-cofx [this cofx-id handler-fn]) + (inject-cofx + [this cofx-id] + [this cofx-id value]) + (clear-cofx + [this] + [this cofx-id]) + + ;; events ---- + (clear-event + [this] + [this event-id]) + (reg-event-db + [this id db-handler] + [this id interceptors db-handler]) + (reg-event-fx + [this id fx-handler] + [this id interceptors fx-handler]) + (reg-event-ctx + [this id handler] + [this id interceptors handler]) + + ;; errors ---- + (event-error-handler + [this handler])) + +;; connect all the pieces of state ---- +(defrecord Frame [registry event-queue app-db subs-cache default-interceptors] + IFrame + ;; dispatch ---- + (dispatch [this event-v] + (router/dispatch event-queue event-v)) + (dispatch-sync [this event-v] + (router/dispatch-sync this event-v)) + + ;; subs ---- + (reg-sub-raw [this query-id handler-fn] + (reg/register-handler registry subs/kind query-id handler-fn)) + (reg-sub [this query-id arg1] + (subs/reg-sub this query-id arg1)) + (reg-sub [this query-id arg1 arg2] + (subs/reg-sub this query-id arg1 arg2)) + (reg-sub [this query-id arg1 arg2 arg3] + (subs/reg-sub this query-id arg1 arg2 arg3)) + (reg-sub [this query-id arg1 arg2 arg3 arg4] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15)) + (reg-sub [this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16] + (subs/reg-sub this query-id arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16)) + (subscribe [this query-v] + (subs/subscribe this query-v)) + (subscribe [this query-v dynv] + (subs/subscribe this query-v dynv)) + (clear-sub [this] + (reg/clear-handlers registry subs/kind)) + (clear-sub [this query-id] + (reg/clear-handlers registry subs/kind query-id)) + (clear-subscriptions-cache [this] + (subs/clear-subscription-cache! subs-cache)) + + ;; fx ---- + (reg-fx [this fx-id handler-fn] + (reg/register-handler registry fx/kind fx-id handler-fn)) + (clear-fx [this] + (reg/clear-handlers registry fx/kind)) + (clear-fx [this fx-id] + (reg/clear-handlers registry fx/kind fx-id)) + + ;; cofx ---- + (reg-cofx [this cofx-id handler-fn] + (reg/register-handler registry cofx/kind cofx-id handler-fn)) + (inject-cofx [this cofx-id] + (cofx/inject-cofx registry cofx-id)) + (inject-cofx [this cofx-id value] + (cofx/inject-cofx registry cofx-id value)) + (clear-cofx [this] + (reg/clear-handlers registry cofx/kind)) + (clear-cofx [this cofx-id] + (reg/clear-handlers registry cofx/kind cofx-id)) + + ;; events ---- + (clear-event [this] + (reg/clear-handlers registry events/kind)) + (clear-event [this id] + (reg/clear-handlers registry events/kind id)) + + (reg-event-db [this id db-handler] + (reg-event-db this id nil db-handler)) + (reg-event-db [this id interceptors db-handler] + (events/register + registry + id + [default-interceptors interceptors (stdi/db-handler->interceptor db-handler)])) + (reg-event-fx [this id fx-handler] + (reg-event-fx this id nil fx-handler)) + (reg-event-fx [this id interceptors fx-handler] + (events/register + registry + id + [default-interceptors interceptors (stdi/fx-handler->interceptor fx-handler)])) + (reg-event-ctx [this id handler] + (reg-event-ctx this id nil handler)) + (reg-event-ctx [this id interceptors handler] + (events/register + registry + id + [default-interceptors interceptors (stdi/ctx-handler->interceptor handler)])) + + (event-error-handler [this handler] + (reg/register-handler registry :error :event-handler handler))) + +(def frame-id (atom 0)) + +(defn make-frame + "Creates a new frame, which bundles the registry (subscriptions, event-handlers, + fx, cofx), app-db, subscription cache, default interceptors, and event queue. + + :registry, :app-db, and :interceptors can be provided through an options map." + [& [{:keys [registry app-db interceptors] :as extra-keys}]] + (let [registry (or registry (reg/make-registry)) + app-db (or app-db (interop/ratom {})) + default-interceptors [(cofx/inject-cofx registry :db) + (fx/do-fx registry) + stdi/inject-global-interceptors] + frame (map->Frame + (merge {:frame-id (swap! frame-id inc) + :registry registry + :app-db app-db + :subs-cache (subs/->SubscriptionCache (atom {})) + :default-interceptors (if interceptors + (if (:replace (meta interceptors)) + interceptors + (into default-interceptors interceptors)) + default-interceptors) + :event-queue (router/->EventQueue :idle interop/empty-queue {} nil)} + (dissoc extra-keys :registry :app-db :interceptors)))] + ;; When events / fx fire, they get their frame from the event-queue + (router/set-frame (:event-queue frame) frame) + frame)) + +(defn make-restore-fn + "Checkpoints the state of re-frame and returns a function which, when + later called, will restore re-frame to that checkpointed state. + + Checkpoint includes app-db, all registered handlers and all subscriptions." + ([frame] + (let [handlers (-> frame :registry :kind->id->handler deref) + app-db (-> frame :app-db deref) + subs-cache (-> frame :subs-cache deref)] + (fn [] + ;; call `dispose!` on all current subscriptions which + ;; didn't originally exist. + (let [original-subs (-> subs-cache vals set) + current-subs (-> frame :subs-cache deref vals)] + (doseq [sub current-subs + :when (not (contains? original-subs sub))] + (interop/dispose! sub))) + + ;; Reset the atoms + ;; We don't need to reset subs-cache, as disposing of the subs + ;; removes them from the cache anyway + (reset! (-> frame :registry :kind->id->handler) handlers) + (reset! (-> frame :app-db) app-db) + nil)))) diff --git a/src/re_frame/fx.cljc b/src/re_frame/fx.cljc index 17cc27ae..d8ee6b76 100644 --- a/src/re_frame/fx.cljc +++ b/src/re_frame/fx.cljc @@ -1,11 +1,10 @@ (ns re-frame.fx (:require [re-frame.router :as router] - [re-frame.db :refer [app-db]] [re-frame.interceptor :refer [->interceptor]] [re-frame.interop :refer [set-timeout!]] [re-frame.events :as events] - [re-frame.registrar :refer [get-handler clear-handlers register-handler]] + [re-frame.registrar :refer [get-handler clear-handlers register-handler *current-frame*]] [re-frame.loggers :refer [console]] [re-frame.trace :as trace :include-macros true])) @@ -14,13 +13,9 @@ (def kind :fx) (assert (re-frame.registrar/kinds kind)) -(defn reg-fx - [id handler] - (register-handler kind id handler)) - ;; -- Interceptor ------------------------------------------------------------- -(def do-fx +(defn do-fx "An interceptor whose `:after` actions the contents of `:effects`. As a result, this interceptor is Domino 3. @@ -42,20 +37,24 @@ You cannot rely on the ordering in which effects are executed, other than that `:db` is guaranteed to be executed first." + [registry] (->interceptor :id :do-fx :after (fn do-fx-after [context] + {:pre [(:frame context)]} (trace/with-trace {:op-type :event/do-fx} (let [effects (:effects context) effects-without-db (dissoc effects :db)] ;; :db effect is guaranteed to be handled before all other effects. (when-let [new-db (:db effects)] - ((get-handler kind :db false) new-db)) + (binding [*current-frame* (:frame context)] + ((get-handler registry kind :db false) new-db))) (doseq [[effect-key effect-value] effects-without-db] - (if-let [effect-fn (get-handler kind effect-key false)] - (effect-fn effect-value) + (if-let [effect-fn (get-handler registry kind effect-key false)] + (binding [*current-frame* (:frame context)] + (effect-fn effect-value)) (console :warn "re-frame: no handler registered for effect:" effect-key @@ -64,126 +63,130 @@ (str "You may be trying to return a coeffect map from an event-fx handler. " "See https://day8.github.io/re-frame/use-cofx-as-fx/")))))))))) -;; -- Builtin Effect Handlers ------------------------------------------------ - -;; :dispatch-later -;; -;; `dispatch` one or more events after given delays. Expects a collection -;; of maps with two keys: :`ms` and `:dispatch` -;; -;; usage: -;; -;; {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]} ;; in 200ms do this: (dispatch [:event-id "param"]) -;; {:ms 100 :dispatch [:also :this :in :100ms]}]} -;; -;; Note: nil entries in the collection are ignored which means events can be added -;; conditionally: -;; {:dispatch-later [ (when (> 3 5) {:ms 200 :dispatch [:conditioned-out]}) -;; {:ms 100 :dispatch [:another-one]}]} -;; -(defn dispatch-later - [{:keys [ms dispatch] :as effect}] - (if (or (empty? dispatch) (not (number? ms))) - (console :error "re-frame: ignoring bad :dispatch-later value:" effect) - (set-timeout! #(router/dispatch dispatch) ms))) - -(reg-fx - :dispatch-later - (fn [value] - (if (map? value) - (dispatch-later value) - (doseq [effect (remove nil? value)] - (dispatch-later effect))))) - -;; :fx -;; -;; Handle one or more effects. Expects a collection of vectors (tuples) of the -;; form [effect-key effect-value]. `nil` entries in the collection are ignored -;; so effects can be added conditionally. -;; -;; usage: -;; -;; {:fx [[:dispatch [:event-id "param"]] -;; nil -;; [:http-xhrio {:method :post -;; ...}]]} -;; - -(reg-fx - :fx - (fn [seq-of-effects] - (if-not (sequential? seq-of-effects) - (console :warn "re-frame: \":fx\" effect expects a seq, but was given " (type seq-of-effects)) - (doseq [[effect-key effect-value] (remove nil? seq-of-effects)] - (when (= :db effect-key) - (console :warn "re-frame: \":fx\" effect should not contain a :db effect")) - (if-let [effect-fn (get-handler kind effect-key false)] - (effect-fn effect-value) - (console :warn "re-frame: in \":fx\" effect found " effect-key " which has no associated handler. Ignoring.")))))) - -;; :dispatch -;; -;; `dispatch` one event. Expects a single vector. -;; -;; usage: -;; {:dispatch [:event-id "param"] } - -(reg-fx - :dispatch - (fn [value] - (if-not (vector? value) - (console :error "re-frame: ignoring bad :dispatch value. Expected a vector, but got:" value) - (router/dispatch value)))) - -;; :dispatch-n -;; -;; `dispatch` more than one event. Expects a list or vector of events. Something for which -;; sequential? returns true. -;; -;; usage: -;; {:dispatch-n (list [:do :all] [:three :of] [:these])} -;; -;; Note: nil events are ignored which means events can be added -;; conditionally: -;; {:dispatch-n (list (when (> 3 5) [:conditioned-out]) -;; [:another-one])} -;; -(reg-fx - :dispatch-n - (fn [value] - (if-not (sequential? value) - (console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, but got:" value) - (doseq [event (remove nil? value)] (router/dispatch event))))) - -;; :deregister-event-handler -;; -;; removes a previously registered event handler. Expects either a single id ( -;; typically a namespaced keyword), or a seq of ids. -;; -;; usage: -;; {:deregister-event-handler :my-id)} -;; or: -;; {:deregister-event-handler [:one-id :another-id]} -;; -(reg-fx - :deregister-event-handler - (fn [value] - (let [clear-event (partial clear-handlers events/kind)] - (if (sequential? value) - (doseq [event value] (clear-event event)) - (clear-event value))))) - -;; :db -;; -;; reset! app-db with a new value. `value` is expected to be a map. -;; -;; usage: -;; {:db {:key1 value1 key2 value2}} -;; -(reg-fx - :db - (fn [value] - (if-not (identical? @app-db value) - (reset! app-db value) - (trace/with-trace {:op-type :reagent/quiescent})))) - +(defn register-built-in! + [{:keys [registry]}] + (let [reg-fx (partial register-handler registry kind)] + + ;; -- Builtin Effect Handlers ------------------------------------------------ + + ;; :dispatch-later + ;; + ;; `dispatch` one or more events after given delays. Expects a collection + ;; of maps with two keys: :`ms` and `:dispatch` + ;; + ;; usage: + ;; + ;; {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]} ;; in 200ms do this: (dispatch [:event-id "param"]) + ;; {:ms 100 :dispatch [:also :this :in :100ms]}]} + ;; + ;; Note: nil entries in the collection are ignored which means events can be added + ;; conditionally: + ;; {:dispatch-later [ (when (> 3 5) {:ms 200 :dispatch [:conditioned-out]}) + ;; {:ms 100 :dispatch [:another-one]}]} + ;; + (letfn [(dispatch-later + [{:keys [ms dispatch] :as effect} {:keys [event-queue]}] + (if (or (empty? dispatch) (not (number? ms))) + (console :error "re-frame: ignoring bad :dispatch-later value:" effect) + (set-timeout! #(router/dispatch event-queue dispatch) ms)))] + + (reg-fx + :dispatch-later + (fn [value] + (if (map? value) + (dispatch-later value *current-frame*) + (doseq [effect (remove nil? value)] + (dispatch-later effect *current-frame*)))))) + + ;; :fx + ;; + ;; Handle one or more effects. Expects a collection of vectors (tuples) of the + ;; form [effect-key effect-value]. `nil` entries in the collection are ignored + ;; so effects can be added conditionally. + ;; + ;; usage: + ;; + ;; {:fx [[:dispatch [:event-id "param"]] + ;; nil + ;; [:http-xhrio {:method :post + ;; ...}]]} + ;; + + (reg-fx + :fx + (fn [seq-of-effects] + (if-not (sequential? seq-of-effects) + (console :warn "re-frame: \":fx\" effect expects a seq, but was given " (type seq-of-effects)) + (doseq [[effect-key effect-value] (remove nil? seq-of-effects)] + (when (= :db effect-key) + (console :warn "re-frame: \":fx\" effect should not contain a :db effect")) + (if-let [effect-fn (get-handler (:registry *current-frame*) kind effect-key false)] + (effect-fn effect-value *current-frame*) + (console :warn "re-frame: in \":fx\" effect found " effect-key " which has no associated handler. Ignoring.")))))) + + ;; :dispatch + ;; + ;; `dispatch` one event. Expects a single vector. + ;; + ;; usage: + ;; {:dispatch [:event-id "param"] } + + (reg-fx + :dispatch + (fn [value] + (if-not (vector? value) + (console :error "re-frame: ignoring bad :dispatch value. Expected a vector, but got:" value) + (router/dispatch (:event-queue *current-frame*) value)))) + + ;; :dispatch-n + ;; + ;; `dispatch` more than one event. Expects a list or vector of events. Something for which + ;; sequential? returns true. + ;; + ;; usage: + ;; {:dispatch-n (list [:do :all] [:three :of] [:these])} + ;; + ;; Note: nil events are ignored which means events can be added + ;; conditionally: + ;; {:dispatch-n (list (when (> 3 5) [:conditioned-out]) + ;; [:another-one])} + ;; + (reg-fx + :dispatch-n + (fn [value] + (if-not (sequential? value) + (console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, but got:" value) + (doseq [event (remove nil? value)] (router/dispatch (:event-queue *current-frame*) event))))) + + ;; :deregister-event-handler + ;; + ;; removes a previously registered event handler. Expects either a single id ( + ;; typically a namespaced keyword), or a seq of ids. + ;; + ;; usage: + ;; {:deregister-event-handler :my-id)} + ;; or: + ;; {:deregister-event-handler [:one-id :another-id]} + ;; + (reg-fx + :deregister-event-handler + (fn [value] + (let [clear-event (partial clear-handlers (:registry *current-frame*) events/kind)] + (if (sequential? value) + (doseq [event value] (clear-event event)) + (clear-event value))))) + + ;; :db + ;; + ;; reset! app-db with a new value. `value` is expected to be a map. + ;; + ;; usage: + ;; {:db {:key1 value1 key2 value2}} + ;; + (reg-fx + :db + (fn [value] + (let [{:keys [app-db]} *current-frame*] + (if-not (identical? @app-db value) + (reset! app-db value) + (trace/with-trace {:op-type :reagent/quiescent}))))))) diff --git a/src/re_frame/interceptor.cljc b/src/re_frame/interceptor.cljc index f3853631..efd5ef18 100644 --- a/src/re_frame/interceptor.cljc +++ b/src/re_frame/interceptor.cljc @@ -130,15 +130,15 @@ (defn- context "Create a fresh context" - ([event interceptors] - (-> {} + ([frame event interceptors] + (-> {:frame frame} (assoc-coeffect :event event) ;; Some interceptors, like `trim-v` and `unwrap`, alter event so capture ;; the original for use cases such as tracing. (assoc-coeffect :original-event event) (enqueue interceptors))) - ([event interceptors db] ;; only used in tests, probably a hack, remove ? XXX - (-> (context event interceptors) + ([frame event interceptors db] ;; only used in tests, probably a hack, remove ? XXX + (-> (context frame event interceptors) (assoc-coeffect :db db)))) (defn- change-direction @@ -228,9 +228,9 @@ of interceptors yet to be processed, and a `:stack` of interceptors already done. In advanced cases, these values can be modified by the functions through which the context is threaded." - [event-v interceptors] - (let [ctx (context event-v interceptors) - error-handler (registrar/get-handler :error :event-handler)] + [frame event-v interceptors] + (let [ctx (context frame event-v interceptors) + error-handler (registrar/get-handler (:registry frame) :error :event-handler)] (trace/merge-trace! {:tags {:interceptors interceptors}}) (if-not error-handler diff --git a/src/re_frame/query/alpha.cljc b/src/re_frame/query/alpha.cljc deleted file mode 100644 index ad67ef93..00000000 --- a/src/re_frame/query/alpha.cljc +++ /dev/null @@ -1,61 +0,0 @@ -(ns re-frame.query.alpha - (:require - [re-frame :as-alias rf] - [re-frame.db :refer [app-db]] - [re-frame.interop :refer [reagent-id]] - [re-frame.loggers :refer [console]] - [re-frame.register.alpha :refer [lifecycle->method]] - [re-frame.registrar :refer [get-handler]] - [re-frame.trace :as trace :include-macros true])) - -(declare lifecycle) - -(defn legacy-lifecycle [v] - (when (vector? v) - (or (lifecycle (meta v)) - :default))) - -(defn legacy-query-id [q] - (when (vector? q) (first q))) - -(def id (some-fn legacy-query-id ::rf/q)) - -(def flow-lifecycle (comp #{:flow} id)) - -(def lifecycle (some-fn flow-lifecycle - legacy-lifecycle - ::rf/lifecycle - (constantly :default))) - -(defn method [q] (@lifecycle->method (lifecycle q))) - -(defn clear-all-methods! [] (reset! lifecycle->method {})) - -(def cache (atom {})) - -(defn cached [q] (if-some [r (get-in @cache [(lifecycle q) q])] - (do (trace/merge-trace! {:tags {:cached? true - :reaction (reagent-id r)}}) - r) - (trace/merge-trace! {:tags {:cached? false}}))) - -(defn cache! [q r] (swap! cache assoc-in [(lifecycle q) q] r) r) - -(defn clear! - ([] (reset! cache {})) - ([q] (clear! q (lifecycle q))) - ([q strat] (swap! cache update strat dissoc q))) - -(defn handle [q] - (let [handler (get-handler :sub (id q))] - (if-not (nil? handler) - (handler app-db q) - (do (trace/merge-trace! {:error true}) - (console :error - "re-frame: no subscription handler registered for: " - (id q) - ". Returning a nil subscription."))))) - -(defn query? [q] - (some? (and (id q) - (lifecycle q)))) diff --git a/src/re_frame/register/alpha.cljc b/src/re_frame/register/alpha.cljc deleted file mode 100644 index 4ffb72d2..00000000 --- a/src/re_frame/register/alpha.cljc +++ /dev/null @@ -1,5 +0,0 @@ -(ns re-frame.register.alpha) - -(defmulti reg (fn [kind & _] kind)) - -(def lifecycle->method (atom {})) diff --git a/src/re_frame/registrar.cljc b/src/re_frame/registrar.cljc index c23cee9d..bb56856a 100644 --- a/src/re_frame/registrar.cljc +++ b/src/re_frame/registrar.cljc @@ -6,48 +6,64 @@ [re-frame.loggers :refer [console]] [re-frame.settings :as settings])) +(def ^:dynamic *current-frame*) + +(defprotocol IRegistry + (get-handler + [this kind] + [this kind id] + [this kind id required?]) + + (register-handler + [this kind id handler-fn]) + + (clear-handlers + [this] + [this kind] + [this kind id])) + ;; kinds of handlers (def kinds #{:event :fx :cofx :sub :error}) -;; This atom contains a register of all handlers. -;; Contains a two layer map, keyed first by `kind` (of handler), and then `id` of handler. -;; Leaf nodes are handlers. -(def kind->id->handler (atom {})) - -(defn get-handler - - ([kind] - (get @kind->id->handler kind)) - - ([kind id] - (-> (get @kind->id->handler kind) - (get id))) - - ([kind id required?] - (let [handler (get-handler kind id)] - (when debug-enabled? ;; This is in a separate `when` so Closure DCE can run ... - (when (and required? (nil? handler)) ;; ...otherwise you'd need to type-hint the `and` with a ^boolean for DCE. - (console :error "re-frame: no" (str kind) "handler registered for:" id))) - handler))) - -(defn register-handler - [kind id handler-fn] - (when debug-enabled? ;; This is in a separate when so Closure DCE can run - (when (and (not (settings/loaded?)) (get-handler kind id false)) - (console :warn "re-frame: overwriting" (str kind) "handler for:" id))) ;; allow it, but warn. Happens on figwheel reloads. - (swap! kind->id->handler assoc-in [kind id] handler-fn) - handler-fn) ;; note: returns the just registered handler - -(defn clear-handlers - ([] ;; clear all kinds - (reset! kind->id->handler {})) - - ([kind] ;; clear all handlers for this kind - (assert (kinds kind)) - (swap! kind->id->handler dissoc kind)) - - ([kind id] ;; clear a single handler for a kind - (assert (kinds kind)) - (if (get-handler kind id) - (swap! kind->id->handler update-in [kind] dissoc id) - (console :warn "re-frame: can't clear" (str kind) "handler for" (str id ". Handler not found."))))) +(defrecord Registry [kind->id->handler] + IRegistry + + (get-handler [this kind] + (get @kind->id->handler kind)) + + (get-handler [this kind id] + (-> (get @kind->id->handler kind) + (get id))) + + (get-handler [this kind id required?] + (let [handler (get-handler this kind id)] + (when debug-enabled? ;; This is in a separate `when` so Closure DCE can run ... + (when (and required? (nil? handler)) ;; ...otherwise you'd need to type-hint the `and` with a ^boolean for DCE. + (console :error "re-frame: no" (str kind) "handler registered for:" id))) + handler)) + + (register-handler [this kind id handler-fn] + (when debug-enabled? ;; This is in a separate when so Closure DCE can run + (when (and (not (settings/loaded?)) (get-handler this kind id false)) + (console :warn "re-frame: overwriting" (str kind) "handler for:" id))) ;; allow it, but warn. Happens on figwheel reloads. + (swap! kind->id->handler assoc-in [kind id] handler-fn) + handler-fn) ;; note: returns the just registered handler + + (clear-handlers [this] ;; clear all kinds + (reset! kind->id->handler {})) + + (clear-handlers [this kind] ;; clear all handlers for this kind + (assert (kinds kind)) + (swap! kind->id->handler dissoc kind)) + + (clear-handlers [this kind id] ;; clear a single handler for a kind + (assert (kinds kind)) + (if (get-handler this kind id) + (swap! kind->id->handler update-in [kind] dissoc id) + (console :warn "re-frame: can't clear" (str kind) "handler for" (str id ". Handler not found."))))) + +(defn make-registry [] + ;; This atom contains a register of all handlers. + ;; Contains a map keyed first by `kind` (of handler), and then `id`. + ;; Leaf nodes are handlers. + (->Registry (atom {}))) diff --git a/src/re_frame/router.cljc b/src/re_frame/router.cljc index 9caddc60..6c880781 100644 --- a/src/re_frame/router.cljc +++ b/src/re_frame/router.cljc @@ -73,6 +73,7 @@ (add-post-event-callback [this id callback-fn]) (remove-post-event-callback [this id]) (purge [this]) + (set-frame [this frame]) ;; -- Implementation via a Finite State Machine (-fsm-trigger [this trigger arg]) @@ -90,6 +91,7 @@ ;; Concrete implementation of IEventQueue (deftype EventQueue [#?(:cljs ^:mutable fsm-state :clj ^:volatile-mutable fsm-state) #?(:cljs ^:mutable queue :clj ^:volatile-mutable queue) + #?(:cljs ^:mutable frame :clj ^:volatile-mutable frame) #?(:cljs ^:mutable post-event-callback-fns :clj ^:volatile-mutable post-event-callback-fns)] IEventQueue @@ -114,6 +116,9 @@ (purge [_] (set! queue empty-queue)) + (set-frame [_ value] + (set! frame value)) + ;; -- FSM Implementation --------------------------------------------------- (-fsm-trigger @@ -173,7 +178,7 @@ [this] (let [event-v (peek queue)] (try - (handle event-v) + (handle frame event-v) (set! queue (pop queue)) (-call-post-event-callbacks this event-v) (catch #?(:cljs :default :clj Exception) ex @@ -214,27 +219,21 @@ (-process-1st-event-in-queue this) ;; do the event which paused processing (-run-queue this))) ;; do the rest of the queued events -;; --------------------------------------------------------------------------- -;; Event Queue -;; When "dispatch" is called, the event is added into this event queue. Later, -;; the queue will "run" and the event will be "handled" by the registered function. -;; -(def event-queue (->EventQueue :idle empty-queue {})) - ;; --------------------------------------------------------------------------- ;; Dispatching ;; (defn dispatch - [event] + [event-queue event] + {:pre [event-queue]} (if (nil? event) (throw (ex-info "re-frame: you called \"dispatch\" without an event vector." {})) (push event-queue event)) nil) ;; Ensure nil return. See https://github.com/day8/re-frame/wiki/Beware-Returning-False (defn dispatch-sync - [event-v] - (handle event-v) - (-call-post-event-callbacks event-queue event-v) ;; slightly ugly hack. Run the registered post event callbacks. + [frame event-v] + (handle frame event-v) + (-call-post-event-callbacks (:event-queue frame) event-v) ;; slightly ugly hack. Run the registered post event callbacks. (trace/with-trace {:op-type :sync}) nil) ;; Ensure nil return. See https://github.com/day8/re-frame/wiki/Beware-Returning-False diff --git a/src/re_frame/std_interceptors.cljc b/src/re_frame/std_interceptors.cljc index 4563425d..930d6e8c 100644 --- a/src/re_frame/std_interceptors.cljc +++ b/src/re_frame/std_interceptors.cljc @@ -4,7 +4,6 @@ [re-frame.interceptor :refer [->interceptor get-effect get-coeffect assoc-coeffect assoc-effect update-coeffect]] [re-frame.loggers :refer [console]] [re-frame.settings :as settings] - [re-frame.db :refer [app-db]] [clojure.data :as data] [re-frame.cofx :as cofx] [re-frame.utils :as utils] diff --git a/src/re_frame/subs.cljc b/src/re_frame/subs.cljc index 667788e8..6a277df9 100644 --- a/src/re_frame/subs.cljc +++ b/src/re_frame/subs.cljc @@ -1,10 +1,9 @@ (ns re-frame.subs (:require - [re-frame.db :refer [app-db]] [re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref? dispose! reagent-id reactive?]] [re-frame.loggers :refer [console]] [re-frame.utils :refer [first-in-vector]] - [re-frame.registrar :refer [get-handler clear-handlers register-handler]] + [re-frame.registrar :refer [get-handler clear-handlers register-handler *current-frame*]] [re-frame.trace :as trace :include-macros true])) (def kind :sub) @@ -15,51 +14,67 @@ ;; De-duplicate subscriptions. If two or more equal subscriptions ;; are concurrently active, we want only one handler running. ;; Two subscriptions are "equal" if their query vectors test "=". -(def query->reaction (atom {})) +(defprotocol ICache + ;; Using -prefixed methods here because for some reason when removing the dash I get + ;; + ;; java.lang.ClassFormatError: Duplicate method name&signature in class file re_frame/subs/SubscriptionCache + ;; + ;; as far as I understand that would happen when you try to implement two identically + ;; named functions in one defrecord call but I don't see how I'm doing that here + (-clear [this]) + (-cache-and-return [this query-v dynv r]) + (-cache-lookup + [this query-v] + [this query-v dyn-v])) + + +(defrecord SubscriptionCache [state] + #?(:cljs IDeref :clj clojure.lang.IDeref) + #?(:cljs (-deref [this] (-> this :state deref)) + :clj (deref [this] (-> this :state deref))) + ICache + (-clear [this] + (doseq [[k rxn] @state] + (dispose! rxn)) + (if (not-empty @state) + (console :warn "re-frame: The subscription cache isn't empty after being cleared"))) + (-cache-and-return [this query-v dynv r] + (let [cache-key [query-v dynv]] + ;; when this reaction is no longer being used, remove it from the cache + (add-on-dispose! r #(trace/with-trace {:operation (first-in-vector query-v) + :op-type :sub/dispose + :tags {:query-v query-v + :reaction (reagent-id r)}} + (swap! state + (fn [query-cache] + (if (and (contains? query-cache cache-key) (identical? r (get query-cache cache-key))) + (dissoc query-cache cache-key) + query-cache))))) + ;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions + (swap! state (fn [query-cache] + (when debug-enabled? + (when (contains? query-cache cache-key) + (console :warn "re-frame: Adding a new subscription to the cache while there is an existing subscription in the cache" cache-key))) + (assoc query-cache cache-key r))) + (trace/merge-trace! {:tags {:reaction (reagent-id r)}}) + r)) ;; return the actual reaction + (-cache-lookup [this query-v] + (-cache-lookup this query-v [])) + (-cache-lookup [this query-v dyn-v] + (get @state [query-v dyn-v]))) (defn clear-subscription-cache! "calls `on-dispose` for each cached item, which will cause the value to be removed from the cache" - [] - (doseq [[k rxn] @query->reaction] - (dispose! rxn)) - (if (not-empty @query->reaction) - (console :warn "re-frame: The subscription cache isn't empty after being cleared"))) + [subs-cache] + (-clear subs-cache)) + (defn clear-all-handlers! "Unregisters all existing subscription handlers" - [] - (clear-handlers kind) - (clear-subscription-cache!)) - -(defn cache-and-return - "cache the reaction r" - [query-v dynv r] - (let [cache-key [query-v dynv]] - ;; when this reaction is no longer being used, remove it from the cache - (add-on-dispose! r #(trace/with-trace {:operation (first-in-vector query-v) - :op-type :sub/dispose - :tags {:query-v query-v - :reaction (reagent-id r)}} - (swap! query->reaction - (fn [query-cache] - (if (and (contains? query-cache cache-key) (identical? r (get query-cache cache-key))) - (dissoc query-cache cache-key) - query-cache))))) - ;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions - (swap! query->reaction (fn [query-cache] - (when debug-enabled? - (when (contains? query-cache cache-key) - (console :warn "re-frame: Adding a new subscription to the cache while there is an existing subscription in the cache" cache-key))) - (assoc query-cache cache-key r))) - (trace/merge-trace! {:tags {:reaction (reagent-id r)}}) - r)) ;; return the actual reaction - -(defn cache-lookup - ([query-v] - (cache-lookup query-v [])) - ([query-v dyn-v] - (get @query->reaction [query-v dyn-v]))) + [{:keys [registry subs-cache]}] + (clear-handlers registry kind) + (-clear subs-cache)) ;; -- subscribe --------------------------------------------------------------- @@ -71,38 +86,38 @@ "https://day8.github.io/re-frame/FAQs/UseASubscriptionInAnEventHandler/"))) (defn subscribe - ([query] + ([{:keys [registry app-db subs-cache frame-id] :as frame} query] (warn-when-not-reactive) (trace/with-trace {:operation (first-in-vector query) :op-type :sub/create :tags {:query-v query}} - (if-let [cached (cache-lookup query)] + (if-let [cached (-cache-lookup subs-cache query)] (do (trace/merge-trace! {:tags {:cached? true :reaction (reagent-id cached)}}) cached) (let [query-id (first-in-vector query) - handler-fn (get-handler kind query-id)] + handler-fn (get-handler registry kind query-id)] (trace/merge-trace! {:tags {:cached? false}}) (if (nil? handler-fn) (do (trace/merge-trace! {:error true}) (console :error (str "re-frame: no subscription handler registered for: " query-id ". Returning a nil subscription."))) - (cache-and-return query [] (handler-fn app-db query))))))) + (-cache-and-return subs-cache query [] (handler-fn frame query))))))) - ([query dynv] + ([{:keys [registry app-db subs-cache frame-id] :as frame} query dynv] (warn-when-not-reactive) (trace/with-trace {:operation (first-in-vector query) :op-type :sub/create :tags {:query-v query :dyn-v dynv}} - (if-let [cached (cache-lookup query dynv)] + (if-let [cached (-cache-lookup subs-cache query dynv)] (do (trace/merge-trace! {:tags {:cached? true :reaction (reagent-id cached)}}) cached) (let [query-id (first-in-vector query) - handler-fn (get-handler kind query-id)] + handler-fn (get-handler registry kind query-id)] (trace/merge-trace! {:tags {:cached? false}}) (when debug-enabled? (when-let [not-reactive (not-empty (remove ratom? dynv))] @@ -111,11 +126,11 @@ (do (trace/merge-trace! {:error true}) (console :error (str "re-frame: no subscription handler registered for: " query-id ". Returning a nil subscription."))) (let [dyn-vals (make-reaction (fn [] (mapv deref dynv))) - sub (make-reaction (fn [] (handler-fn app-db query @dyn-vals)))] + sub (make-reaction (fn [] (handler-fn frame query @dyn-vals)))] ;; handler-fn returns a reaction which is then wrapped in the sub reaction ;; need to double deref it to get to the actual value. ;(console :log "Subscription created: " v dynv) - (cache-and-return query dynv (make-reaction (fn [] @@sub)))))))))) + (-cache-and-return subs-cache query dynv (make-reaction (fn [] @@sub)))))))))) ;; -- reg-sub ----------------------------------------------------------------- @@ -155,7 +170,7 @@ (trace/merge-trace! {:tags {:input-signals (doall (to-seq (map-signals reagent-id signals)))}}) dereffed-signals)) -(defn sugar [query-id sub-fn query? & args] +(defn sugar [{:keys [app-db] :as frame} query-id sub-fn query? & args] (let [error-header (str "re-frame: reg-sub for " query-id ", ") [op f :as comp-f] (take-last 2 args) [input-args ;; may be empty, or one signal fn, or pairs of :<- / vector @@ -191,15 +206,16 @@ 1 (let [f (first input-args)] (when-not (fn? f) (console :error error-header "2nd argument expected to be an inputs function, got:" f)) - f) + #(binding [*current-frame* frame] + (apply f %&))) ;; one sugar pair 2 (let [[marker vec] input-args] (when-not (= :<- marker) (console :error error-header "expected :<-, got:" marker)) (fn inp-fn - ([_] (sub-fn vec)) - ([_ _] (sub-fn vec)))) + ([_] (sub-fn frame vec)) + ([_ _] (sub-fn frame vec)))) ;; multiple sugar pairs (let [pairs (partition 2 input-args) @@ -208,19 +224,21 @@ (when-not (and (every? #{:<-} markers) (every? query? vecs)) (console :error error-header "expected pairs of :<- and vectors, got:" pairs)) (fn inp-fn - ([_] (map sub-fn vecs)) - ([_ _] (map sub-fn vecs)))))] + ([_] (map (partial sub-fn frame) vecs)) + ([_ _] (map (partial sub-fn frame) vecs)))))] [inputs-fn computation-fn])) (defn reg-sub - [query-id & args] - (let [[inputs-fn computation-fn] (apply sugar query-id subscribe vector? args)] + [{:keys [registry]} query-id & args] + (do (register-handler + registry kind query-id (fn subs-handler-fn - ([db query-vec] - (let [subscriptions (inputs-fn query-vec nil) + ([frame query-vec] + (let [[inputs-fn computation-fn] (apply sugar frame query-id subscribe vector? args) + subscriptions (inputs-fn query-vec nil) reaction-id (atom nil) reaction (make-reaction (fn [] @@ -228,14 +246,16 @@ :op-type :sub/run :tags {:query-v query-vec :reaction @reaction-id}} - (let [subscription (computation-fn (deref-input-signals subscriptions query-id) query-vec)] - (trace/merge-trace! {:tags {:value subscription}}) - subscription))))] + (binding [*current-frame* frame] + (let [subscription (computation-fn (deref-input-signals subscriptions query-id) query-vec)] + (trace/merge-trace! {:tags {:value subscription}}) + subscription)))))] (reset! reaction-id (reagent-id reaction)) reaction)) - ([db query-vec dyn-vec] - (let [subscriptions (inputs-fn query-vec dyn-vec) + ([frame query-vec dyn-vec] + (let [[inputs-fn computation-fn] (apply sugar frame query-id subscribe vector? args) + subscriptions (inputs-fn query-vec dyn-vec) reaction-id (atom nil) reaction (make-reaction (fn [] @@ -244,9 +264,10 @@ :tags {:query-v query-vec :dyn-v dyn-vec :reaction @reaction-id}} - (let [subscription (computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec)] - (trace/merge-trace! {:tags {:value subscription}}) - subscription))))] + (binding [*current-frame* frame] + (let [subscription (computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec)] + (trace/merge-trace! {:tags {:value subscription}}) + subscription)))))] (reset! reaction-id (reagent-id reaction)) reaction)))))) diff --git a/src/re_frame/subs/alpha.cljc b/src/re_frame/subs/alpha.cljc deleted file mode 100644 index eb19baff..00000000 --- a/src/re_frame/subs/alpha.cljc +++ /dev/null @@ -1,111 +0,0 @@ -(ns re-frame.subs.alpha - (:require - [re-frame.subs :refer [deref-input-signals sugar warn-when-not-reactive]] - [re-frame.registrar :refer [register-handler]] - [re-frame.register.alpha :refer [reg lifecycle->method]] - [re-frame.interop :refer [add-on-dispose! make-reaction reactive? reagent-id ratom]] - [re-frame.query.alpha :as q] - [re-frame :as-alias rf] - [re-frame.trace :as trace :include-macros true] - [re-frame.flow.alpha :as flow])) - -(defmethod reg :sub-lifecycle [_ k f] - (swap! lifecycle->method assoc - k - (fn [q] - (trace/with-trace {:operation (q/id q) - :op-type :sub/create - :tags {:query q}} - (f q))))) - -(defn sub - ([q] - (if (keyword? q) - (sub q {}) - (let [md (q/method q)] - (cond (map? q) (md q) - (vector? q) (md {::rf/q (q/id q) - ::rf/lifecycle (q/lifecycle q) - ::rf/query-v q}))))) - ([id q] - (sub (assoc q ::rf/q id)))) - -(defmethod reg :sub [kind id & args] - (let [[inputs-fn computation-fn] (apply sugar id sub q/query? args)] - (register-handler - kind - id - (fn subs-handler-fn [_ q] - (let [subscriptions (inputs-fn q nil) - rid (atom nil) - r (make-reaction - #(trace/with-trace {:operation (q/id q) - :op-type :sub/run - :tags {:query q - :reaction @rid}} - (let [subscription (computation-fn - (deref-input-signals subscriptions id) - q)] - (trace/merge-trace! {:tags {:value subscription}}) - subscription)))] - (reset! rid (reagent-id r)) - r))))) - -(defmethod reg :legacy-sub [_ id & args] - (let [[inputs-fn computation-fn] (apply sugar id sub q/query? args)] - (register-handler - :sub - id - (fn subs-handler-fn [_ q] - (let [subscriptions (inputs-fn q nil) - rid (atom nil) - r (make-reaction - #(trace/with-trace {:operation (q/id q) - :op-type :sub/run - :tags {:query q - :reaction @rid}} - (let [q (if (map? q) - (-> (or (::rf/query-v q) [(q/id q)]) - (vary-meta assoc ::rf/lifecycle (q/lifecycle q))) - q) - subscription (computation-fn - (deref-input-signals subscriptions id) - q)] - (trace/merge-trace! {:tags {:value subscription}}) - subscription)))] - (reset! rid (reagent-id r)) - r))))) - -(defn sub-reactive [q] - (warn-when-not-reactive) - (or (q/cached q) - (let [md (q/lifecycle q) - r (q/handle q)] - (add-on-dispose! r #(q/clear! q md)) - (q/cache! q r)))) - -(reg :sub-lifecycle :reactive sub-reactive) - -(defn sub-safe [q] - (if (reactive?) - (sub-reactive q) - (or (q/cached q) - (q/handle q)))) - -(reg :sub-lifecycle :safe sub-safe) -(reg :sub-lifecycle :default sub-safe) - -(defn sub-forever [q] - (or (q/cached q) - (q/cache! q (q/handle q)))) - -(reg :sub-lifecycle :forever sub-forever) - -(def nil-ref (ratom nil)) - -(defn sub-flow [q] - (or (some-> (:id (or (second (::rf/query-v q)) q)) - flow/lookup meta :re-frame.flow.alpha/ref) - nil-ref)) - -(reg :sub-lifecycle :flow sub-flow) diff --git a/test/re_frame/flow/alpha_test.cljc b/test/re_frame/flow/alpha_test.cljc deleted file mode 100644 index 6beb83cd..00000000 --- a/test/re_frame/flow/alpha_test.cljc +++ /dev/null @@ -1,129 +0,0 @@ -(ns re-frame.flow.alpha-test - (:require - [cljs.test :refer [is deftest use-fixtures testing]] - [re-frame.alpha :as rf] - [re-frame.flow.alpha :as f] - [re-frame.db :refer [app-db]])) - -(use-fixtures :each (fn [f] (f) (reset! f/flows {}))) - -(deftest abstractions - (is (f/db-path? [])) - (is (f/flow? {})) - (is (f/flow<-? {::f/flow<- :a})) - (is (f/flow<-? (f/flow<- :a)))) - -(deftest helpers - (let [c {:id :c - :inputs {:db-path [:some :path] - :flow-a (rf/flow<- :a)} - :live-inputs {:db-path [:some :path] - :flow-b (rf/flow<- :b)} - :path [:x :y :z]}] - (rf/reg-flow c) - (rf/reg-flow {:id :a}) - (rf/reg-flow {:id :b}) - (testing "registration" - (is (= :a (:id (f/lookup :a))))) - (testing "topological sort" - (is (= #{:a :b} (set (f/input-ids c)))) - (is (= :c (last (map :id (f/topsort @f/flows)))))) - - (testing "stale inputs & outputs" - (is (= {:flow-a [:a]} (f/stale-in-flows {:flow-a {:path [:a]}} - {:inputs {:a [:a]}}))) - - (is (= {:flow-a {:b [:b]}} (f/stale-out-flows {:flow-a {:inputs {:b [:b]}}} - {:path [:b]})))))) - -(deftest registry - (testing "reg-flow" - (rf/reg-flow {:id :a}) - (is (some? (f/lookup :a))) - (rf/reg-flow :b {}) - (is (some? (f/lookup :b)))) - (testing "clear-flow" - (rf/clear-flow :a) - (is (nil? (f/lookup :a))) - (rf/reg-flow {:id :c}) - (rf/clear-flow) - (is (nil? (f/lookup :b))) - (is (nil? (f/lookup :c))))) - -(deftest get-flow - (rf/reg-flow {:id :x :path [:a :b]}) - (let [db {:a {:b :y}}] - (is (= :y (rf/get-flow db :x))))) - -(deftest run-flow - (rf/reg-event-db :go-live (fn [db _] (assoc db :l? true))) - (rf/reg-event-db :go-dead (fn [db _] (dissoc db :l?))) - (rf/reg-flow {:id :live-db - :live? :l?}) - (rf/reg-flow {:id :live-path - :live? :l? - :live-inputs {:l? [:l?]}}) - (rf/reg-flow {:id :live-input - :live? :l? - :live-inputs {:l? (rf/flow<- :live-path)}}) - (testing "basic flow" - (rf/reg-event-fx :basic-event (fn [_ _] {})) - (rf/reg-flow {:id :basic-flow}) - (is (not (rf/get-flow @app-db :basic-flow))) - (rf/dispatch-sync [:basic-event]) - (is (rf/get-flow @app-db :basic-flow))) - (testing "live flow" - (is (nil? (rf/get-flow @app-db :live-db))) - (is (nil? (rf/get-flow @app-db :live-path))) - (is (nil? (rf/get-flow @app-db :live-input))) - (rf/dispatch-sync [:go-live]) - (is (rf/get-flow @app-db :live-db)) - (is (rf/get-flow @app-db :live-path)) - (is (rf/get-flow @app-db :live-input)) - (rf/dispatch-sync [:go-dead]) - (is (nil? (rf/get-flow @app-db :live-db))) - (is (nil? (rf/get-flow @app-db :live-path))) - (is (nil? (rf/get-flow @app-db :live-input)))) - (testing "flow effects" - (let [flow-a {:id :fx-flow - :output (fn [_] :a)} - flow-b {:id :fx-flow - :output (constantly :b)}] - (rf/reg-event-fx :reg-a (fn [_ _] {:fx [[:reg-flow flow-a]]})) - (rf/reg-event-fx :reg-b (fn [_ _] {:fx [[:reg-flow flow-b]]})) - (rf/reg-event-fx :clear-fx (fn [_ _] {:fx [[:clear-flow :fx-flow]]})) - (is (nil? (rf/get-flow @app-db :fx-flow))) - (rf/dispatch-sync [:reg-a]) - (is (= :a (rf/get-flow @app-db :fx-flow))) - (rf/dispatch-sync [:reg-b]) - (is (= :b (rf/get-flow @app-db :fx-flow))) - (rf/dispatch-sync [:clear-fx]) - (is (nil? (rf/get-flow @app-db :fx-flow))))) - (testing "flow run count" - (let [ct (atom 0) - live-ct (atom 0) - ct-flow {:id :ct-flow - :live-inputs {:l? [:l?]} - :live? (fn [{:keys [l?]}] (swap! live-ct inc) l?) - :output (fn [_] (swap! ct inc))}] - (rf/reg-flow ct-flow) - (rf/dispatch-sync [:go-live]) - (is (= 1 @ct)) - (is (= 1 @live-ct)) - (rf/dispatch-sync [:go-dead]) - (is (= 1 @ct)) - (is (= 3 @live-ct)) - (rf/dispatch-sync [:go-live]) - (is (= 2 @ct)) - (is (= 5 @live-ct)) - (rf/clear-flow :ct-flow) - (rf/dispatch-sync [:go-live]) - (is (= 2 @ct)) - (is (= 6 @live-ct)) - (rf/dispatch-sync [:go-live]) - (is (= 2 @ct)) - (is (= 6 @live-ct)) - (rf/reg-flow ct-flow) - (rf/dispatch-sync [:go-live]) - (is (= 3 @ct)) - (is (= 7 @live-ct))))) diff --git a/test/re_frame/interceptor_test.cljs b/test/re_frame/interceptor_test.cljs index 93582ced..9e8bb606 100644 --- a/test/re_frame/interceptor_test.cljs +++ b/test/re_frame/interceptor_test.cljs @@ -1,6 +1,8 @@ (ns re-frame.interceptor-test (:require [cljs.test :refer-macros [is deftest testing use-fixtures]] + [re-frame.frame :as frame] [reagent.ratom :refer [atom]] + [re-frame.frame] [re-frame.core :refer [reg-global-interceptor clear-global-interceptor]] [re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect update-effect update-coeffect ->interceptor]] @@ -12,6 +14,13 @@ (enable-console-print!) +(def ^:dynamic *frame*) + +(defn set-frame + [f] + (binding [*frame* (frame/make-frame)] + (f))) + (defn global-interceptor-fixture [f] (reg-global-interceptor {:id :interceptor-test @@ -24,15 +33,15 @@ (defn error-handler-fixture [f] - (let [original-handler (registrar/get-handler :error :event-handler)] + (let [original-handler (registrar/get-handler (:registry *frame*) :error :event-handler)] (f) - (registrar/register-handler :error :event-handler original-handler))) + (registrar/register-handler (:registry *frame*) :error :event-handler original-handler))) -(use-fixtures :once global-interceptor-fixture) +(use-fixtures :once set-frame global-interceptor-fixture) (use-fixtures :each error-handler-fixture) (deftest test-trim-v - (let [ctx (context [:event-id :b :c] []) + (let [ctx (context *frame* [:event-id :b :c] []) ctx-trimmed ((:before trim-v) ctx) ctx-untrimmed ((:after trim-v) ctx-trimmed)] (is (= (get-coeffect ctx-trimmed :event) @@ -45,7 +54,7 @@ (let [db {:showing true :another 1} p1 (path [:showing])] ;; a simple one level path - (let [b4 (-> (context [] [] db) + (let [b4 (-> (context *frame* [] [] db) ((:before p1))) ;; before a (-> b4 (assoc-effect :db false) @@ -60,7 +69,7 @@ (let [db {:1 {:2 :target}} p (path [:1 :2])] ;; a two level path - (let [b4 (-> (context [] [] db) + (let [b4 (-> (context *frame* [] [] db) ((:before p)))] ;; before (is (= (get-coeffect b4 :db)) ;; test before @@ -82,7 +91,7 @@ (deftest path-with-no-db-returned (let [path-interceptor (path :a)] - (-> (context [] [path-interceptor] {:a 1}) + (-> (context *frame* [] [path-interceptor] {:a 1}) (interceptor/invoke-interceptors :before) interceptor/change-direction (interceptor/invoke-interceptors :after) @@ -91,7 +100,7 @@ (is)))) (deftest test-inject-global-interceptors - (let [forward (-> (context [] [inject-global-interceptors] {:a 1}) + (let [forward (-> (context *frame* [] [inject-global-interceptors] {:a 1}) (interceptor/invoke-interceptors :before)) _ (is (= {:direction :before} (get-coeffect forward :global))) reverse (-> forward @@ -110,7 +119,7 @@ :new-db-val) i1 (db-handler->interceptor handler) - db (-> (context event [] :original-db-val) + db (-> (context *frame* event [] :original-db-val) ((:before i1)) ;; calls handler - causing :db in :effects to change (get-effect :db))] (is (= db :new-db-val)))) @@ -129,7 +138,7 @@ effect) i1 (fx-handler->interceptor handler) - e (-> (context event [] (:db coeffect)) + e (-> (context *frame* event [] (:db coeffect)) ((:before i1)) ;; call the handler (get-effect))] (is (= e {:db 5 :dispatch [:a]})))) @@ -148,18 +157,18 @@ orig-db {:a 0 :b 2}] (is (= {:a 0 :b 2} - (-> (context [] [] orig-db) + (-> (context *frame* [] [] orig-db) ((:before no-change-handler-i)) ;; no change to :a and :b ((:after change-i)) (get-effect :db)))) (is (= {:a 10 :b 2 :c 12} - (-> (context [] [] orig-db) + (-> (context *frame* [] [] orig-db) ((:before change-handler-i)) ;; cause change to :a ((:after change-i)) (get-effect :db)))) (is (= ::not-found - (-> (context [] [] orig-db) + (-> (context *frame* [] [] orig-db) ((:before no-db-handler-i)) ;; no db effect in context ((:after change-i)) (get-effect :db ::not-found)))))) @@ -167,18 +176,18 @@ (deftest test-after (testing "when no db effect is returned" (let [after-db-val (atom nil)] - (-> (context [:a :b] - [(after (fn [db] (reset! after-db-val db)))] - {:a 1}) + (-> (context *frame* [:a :b] + [(after (fn [db] (reset! after-db-val db)))] + {:a 1}) (interceptor/invoke-interceptors :before) interceptor/change-direction (interceptor/invoke-interceptors :after)) (is (= @after-db-val {:a 1})))) (testing "when a false db effect is returned" (let [after-db-val (atom :not-reset)] - (-> (context [:a :b] - [(after (fn [db] (reset! after-db-val db)))] - {:a 2}) + (-> (context *frame* [:a :b] + [(after (fn [db] (reset! after-db-val db)))] + {:a 2}) (assoc-effect :db nil) (interceptor/invoke-interceptors :before) interceptor/change-direction @@ -186,9 +195,9 @@ (is (= @after-db-val nil)))) (testing "when a nil db effect is returned" (let [after-db-val (atom :not-reset)] - (-> (context [:a :b] - [(after (fn [db] (reset! after-db-val db)))] - {:a 3}) + (-> (context *frame* [:a :b] + [(after (fn [db] (reset! after-db-val db)))] + {:a 3}) (assoc-effect :db false) (interceptor/invoke-interceptors :before) interceptor/change-direction @@ -198,7 +207,7 @@ (deftest test-enrich (testing "when no db effect is returned" (let [db {:a 1} - ctx (context [] [] db) + ctx (context *frame* [] [] db) enrich-interceptor (enrich (fn [db _] db))] (is (= ::not-found (get-effect ctx :db ::not-found))) (is (get-effect ((:after enrich-interceptor) ctx) :db)) @@ -206,7 +215,7 @@ (testing "uses db returned by f" (let [update-fn (fn [db _] (update db :a inc)) updater (db-handler->interceptor update-fn) - ctx (context [] [updater] {:a 1}) + ctx (context *frame* [] [updater] {:a 1}) enrich-interceptor (enrich update-fn) result (-> ctx ((:before updater)) @@ -214,7 +223,7 @@ (is (= {:a 3} (get-effect result :db))))) (testing "uses given db if f returns nil" (let [updater (db-handler->interceptor (fn [db _] (update db :a inc))) - ctx (context [] [updater] {:a 1}) + ctx (context *frame* [] [updater] {:a 1}) enrich-interceptor (enrich (fn [_ _] nil)) result (-> ctx ((:before updater)) @@ -274,7 +283,7 @@ (testing "throws via exception->ex-info" ;; actual handler doesn't matter here, we just need a registered handler so invoke-exception - (registrar/register-handler :error :event-handler identity) + (registrar/register-handler (:registry *frame*) :error :event-handler identity) (try (let [exception (ex-info "Oopsie" {:foo :bar}) interceptor {:id :throws @@ -291,7 +300,7 @@ :interceptor :throws} (ex-data e)))))) (finally - (registrar/clear-handlers :error)))))) + (registrar/clear-handlers (:registry *frame*) :error)))))) (deftest test-exceptions (let [error-atom (atom nil) @@ -303,20 +312,20 @@ interceptors [throws-before]] (testing "an exception in an interceptor, without error handler" - (registrar/register-handler :error :event-handler nil) - (is (nil? (registrar/get-handler :error :event-handler))) + (registrar/register-handler (:registry *frame*) :error :event-handler nil) + (is (nil? (registrar/get-handler (:registry *frame*) :error :event-handler))) (try - (interceptor/execute [:_] interceptors) + (interceptor/execute *frame* [:_] interceptors) (is false "interceptor should have thrown") (catch :default e (is (= "Thrown from interceptor" (ex-message e))))) (is (nil? @error-atom))) (testing "an exception in an interceptor, with error handler" - (registrar/register-handler :error :event-handler error-handler) + (registrar/register-handler (:registry *frame*) :error :event-handler error-handler) (try - (is (registrar/get-handler :error)) - (interceptor/execute [:_] interceptors) + (is (registrar/get-handler (:registry *frame*) :error)) + (interceptor/execute *frame* [:_] interceptors) (let [[original-error re-frame-error] @error-atom {:keys [direction event-v interceptor]} (ex-data re-frame-error)] (is (= "Interceptor Exception: Thrown from interceptor" @@ -329,4 +338,4 @@ (is (= [:_] event-v)) (is (= :throws-before interceptor))) (finally - (registrar/clear-handlers :error)))))) + (registrar/clear-handlers (:registry *frame*) :error)))))) diff --git a/test/re_frame/restore_test.cljs b/test/re_frame/restore_test.cljs index b867acc9..789ac4ec 100644 --- a/test/re_frame/restore_test.cljs +++ b/test/re_frame/restore_test.cljs @@ -1,22 +1,39 @@ (ns re-frame.restore-test (:require [cljs.test :refer-macros [is deftest async use-fixtures testing]] - [re-frame.core :refer [make-restore-fn reg-sub subscribe]] + [re-frame.frame :as frame :refer [make-restore-fn reg-sub subscribe]] [re-frame.subs :as subs])) ;; TODO: future tests in this area could check DB state and registrations are being correctly restored. -(use-fixtures :each {:before subs/clear-all-handlers!}) +(def ^:dynamic *frame*) + +(defn set-frame + [f] + (binding [*frame* (frame/make-frame)] + (f))) + +(use-fixtures :once set-frame) + + +(defn clear-all-handlers! + [f] + (subs/clear-all-handlers! *frame*) + (f)) + +(use-fixtures :each clear-all-handlers!) (defn one? [x] (= 1 x)) (defn two? [x] (= 2 x)) (defn register-test-subs [] (reg-sub + *frame* :test-sub (fn [db ev] (:test-sub db))) (reg-sub + *frame* :test-sub2 (fn [db ev] (:test-sub2 db)))) @@ -24,51 +41,51 @@ (deftest make-restore-fn-test (testing "no existing subs, then making one subscription" (register-test-subs) - (let [original-subs @subs/query->reaction - restore-fn (make-restore-fn)] + (let [original-subs @(:subs-cache *frame*) + restore-fn (make-restore-fn *frame*)] (is (zero? (count original-subs))) - @(subscribe [:test-sub]) - (is (one? (count @subs/query->reaction))) - (is (contains? @subs/query->reaction [[:test-sub] []])) + @(subscribe *frame* [:test-sub]) + (is (one? (count @(:subs-cache *frame*)))) + (is (contains? @(:subs-cache *frame*) [[:test-sub] []])) (restore-fn) - (is (zero? (count @subs/query->reaction)))))) + (is (zero? (count @(:subs-cache *frame*))))))) (deftest make-restore-fn-test2 (testing "existing subs, making more subscriptions" (register-test-subs) - @(subscribe [:test-sub]) - (let [original-subs @subs/query->reaction - restore-fn (make-restore-fn)] + @(subscribe *frame* [:test-sub]) + (let [original-subs @(:subs-cache *frame*) + restore-fn (make-restore-fn *frame*)] (is (one? (count original-subs))) - @(subscribe [:test-sub2]) - (is (contains? @subs/query->reaction [[:test-sub2] []])) - (is (two? (count @subs/query->reaction))) + @(subscribe *frame* [:test-sub2]) + (is (contains? @(:subs-cache *frame*) [[:test-sub2] []])) + (is (two? (count @(:subs-cache *frame*)))) (restore-fn) - (is (not (contains? @subs/query->reaction [[:test-sub2] []]))) - (is (one? (count @subs/query->reaction)))))) + (is (not (contains? @(:subs-cache *frame*) [[:test-sub2] []]))) + (is (one? (count @(:subs-cache *frame*))))))) (deftest make-restore-fn-test3 (testing "existing subs, making more subscriptions with different params on same subscriptions" (register-test-subs) - @(subscribe [:test-sub]) - (let [original-subs @subs/query->reaction - restore-fn (make-restore-fn)] + @(subscribe *frame* [:test-sub]) + (let [original-subs @(:subs-cache *frame*) + restore-fn (make-restore-fn *frame*)] (is (one? (count original-subs))) - @(subscribe [:test-sub :extra :params]) - (is (two? (count @subs/query->reaction))) + @(subscribe *frame* [:test-sub :extra :params]) + (is (two? (count @(:subs-cache *frame*)))) (restore-fn) - (is (one? (count @subs/query->reaction)))))) + (is (one? (count @(:subs-cache *frame*))))))) (deftest nested-restores (testing "running nested restores" (register-test-subs) - (let [restore-fn-1 (make-restore-fn) - _ @(subscribe [:test-sub]) - _ (is (one? (count @subs/query->reaction))) - restore-fn-2 (make-restore-fn)] - @(subscribe [:test-sub2]) - (is (two? (count @subs/query->reaction))) + (let [restore-fn-1 (make-restore-fn *frame*) + _ @(subscribe *frame* [:test-sub]) + _ (is (one? (count @(:subs-cache *frame*)))) + restore-fn-2 (make-restore-fn *frame*)] + @(subscribe *frame* [:test-sub2]) + (is (two? (count @(:subs-cache *frame*)))) (restore-fn-2) - (is (one? (count @subs/query->reaction))) + (is (one? (count @(:subs-cache *frame*)))) (restore-fn-1) - (is (zero? (count @subs/query->reaction)))))) + (is (zero? (count @(:subs-cache *frame*))))))) diff --git a/test/re_frame/router_test.clj b/test/re_frame/router_test.clj index 2f5808cb..6537fc04 100644 --- a/test/re_frame/router_test.clj +++ b/test/re_frame/router_test.clj @@ -1,37 +1,53 @@ (ns re-frame.router-test (:require [clojure.test :refer :all] - [re-frame.core :as rf] + [re-frame.frame :as frame] [re-frame.db :as db])) +(def ^:dynamic *frame*) + +(defn set-frame + [f] + (binding [*frame* (frame/make-frame)] + (f))) + +(use-fixtures :once set-frame) + (defn fixture-re-frame [f] - (let [restore-re-frame (re-frame.core/make-restore-fn)] + (let [restore-re-frame (frame/make-restore-fn *frame*)] (f) (restore-re-frame))) (use-fixtures :each fixture-re-frame) -(rf/reg-event-db - ::test - (fn [db [_ i]] - (update db ::test (fnil conj []) i))) +(defn register + [] + (frame/reg-event-db + *frame* + ::test + (fn [db [_ i]] + (update db ::test (fnil conj []) i))) + + (frame/reg-fx + *frame* + ::promise + (fn [{:keys [p val]}] + (deliver p val))) -(rf/reg-fx - ::promise - (fn [{:keys [p val]}] - (deliver p val))) + (frame/reg-event-fx + *frame* + ::sentinel + (fn [cofx [_ p val]] + {::promise {:p p :val val}}))) -(rf/reg-event-fx - ::sentinel - (fn [cofx [_ p val]] - {::promise {:p p :val val}})) +(use-fixtures :once register) (deftest dispatching-race-condition-469-test ;; Checks for day8/re-frame#469 (let [p (promise)] (is (nil? (dotimes [i 1000] - (rf/dispatch [::test i])))) - (is (nil? (rf/dispatch [::sentinel p ::done]))) + (frame/dispatch *frame* [::test i])))) + (is (nil? (frame/dispatch *frame* [::sentinel p ::done]))) (let [val (deref p 1000 ::timed-out)] (is (= ::done val))) (is (= (::test @db/app-db) diff --git a/test/re_frame/subs/alpha_test.cljs b/test/re_frame/subs/alpha_test.cljs deleted file mode 100644 index 0aee992f..00000000 --- a/test/re_frame/subs/alpha_test.cljs +++ /dev/null @@ -1,148 +0,0 @@ -(ns re-frame.subs.alpha-test - (:require [cljs.test :as test :refer-macros [is deftest testing]] - [reagent.ratom :as r :refer-macros [reaction]] - [re-frame.db :as db] - [re-frame.interop :refer [reactive?]] - [re-frame :as-alias rf] - [re-frame.alpha :refer [reg sub]] - [re-frame.query.alpha :as q] - [re-frame.register.alpha :as reg] - [re-frame.core :refer [reg-sub-raw]] - [re-frame.subs :refer [clear-all-handlers!]])) - -(test/use-fixtures :each {:before #(do (clear-all-handlers!) - (q/clear!))}) - -(def queries - {::map - {::rf/q ::map} - ::map-safe - {::rf/q ::map-safe - ::rf/lifecycle :safe} - ::map-reactive - {::rf/q ::map-reactive - ::rf/lifecycle :reactive} - ::map-forever - {::rf/q ::map-forever - ::rf/lifecycle :forever} - ::query-v - {::rf/q ::query-v - ::rf/query-v [::query-v 1 2 3]} - ::vec - [::vec 1 2 3] - ::vec-reactive - ^{::rf/lifecycle :reactive} - [::vec-reactive 1 2 3]}) - -(defn report [_db q] - {:query q - :lifecycle (q/lifecycle q) - :query-id (q/id q) - :method (q/method q)}) - -(deftest test-sub-method-registration - (testing "Method registration" - (reg :sub-lifecycle :test q/handle) - (is (fn? (get @reg/lifecycle->method :test))))) - -(deftest test-query-api - (testing "Query ID" - (doseq [[qid q] queries] - (is (= qid (q/id q))))) - (testing "Method lookup" - (doseq [[_ q] queries] - (is (fn? (q/method q))))) - - (testing "Lifecycle" - (doseq [k [::map ::query-v ::vec] - :let [q (queries k)]] - (is (= :default (q/lifecycle q)))) - (doseq [k [::map-reactive ::vec-reactive] - :let [q (queries k)]] - (is (= :reactive (q/lifecycle q)))))) - -(deftest test-subscription - (testing "Subscription" - (doseq [[qid q] queries - :let [_ (reg :sub qid report) - result @(sub q)]] - (is (map? (:query result))) - (is (= (:lifecycle result) (q/lifecycle q))) - (is (= (:query-id result) qid)) - (is (= (:method result) (q/method q)))))) - -(deftest test-legacy-subscription - (testing "Legacy Subscription" - (doseq [[qid q] queries - :let [_ (reg :legacy-sub qid report) - result @(sub q)]] - (is (vector? (:query result))) - (is (= (:lifecycle result) (q/lifecycle q))) - (is (= (:query-id result) qid)) - (is (= (:method result) (q/method q)))))) - -(deftest test-caching - (testing "Caching a subscription value" - (let [test-query {:test-sub "cache"}] - (q/cache! test-query 123) - (is (= (q/cached test-query) 123)))) - - (testing "Clearing cache" - (q/clear!) - (is (= @q/cache {})))) - -(def side-effect-atom (atom 0)) - -(defn safe-test [q] - (q/clear!) - (reset! side-effect-atom 0) - (let [test-sub (sub q)] - (reset! db/app-db {:test true}) - (is (= {:test true} @test-sub)) - (is (= @side-effect-atom 1)) - ;; no caching is done - (sub q) - (is (= @side-effect-atom 2)) - (with-redefs [reactive? (constantly true)] - (sub q) - (is (= @side-effect-atom 3)) - ;; now the sub is cached - (sub q) - (is (= @side-effect-atom 3))) - ;; cached sub is now available outside reactive context - (sub q) - (is (= @side-effect-atom 3)))) - -(deftest test-subscription-lifecycles - (reg-sub-raw - :side-effecting-handler - (fn side-effect [db _] - (swap! side-effect-atom inc) - (reaction @db))) - - (testing "Default subscription lifecycle" - (safe-test {::rf/q :side-effecting-handler})) - - (testing "Safe subscription lifecycle" - (safe-test {::rf/q :side-effecting-handler - ::rf/lifecycle :safe})) - - (q/clear!) - (reset! side-effect-atom 0) - - (testing "Reactive subscription lifecycle" - (let [q {::rf/q :side-effecting-handler - ::rf/lifecycle :reactive}] - (reset! db/app-db {:test true}) - (is (= {:test true} @(sub q))) - (is (= @side-effect-atom 1)) - ;; sub is cached, even outside a reactive context - (sub q) - (is (= @side-effect-atom 1)) - (with-redefs [reactive? (constantly true)] - (sub q) - (is (= @side-effect-atom 1)) - (sub q) - (is (= @side-effect-atom 1))) - (sub q) - (is (= @side-effect-atom 1))))) diff --git a/test/re_frame/subs_test.clj b/test/re_frame/subs_test.clj index 304267aa..c95b0783 100644 --- a/test/re_frame/subs_test.clj +++ b/test/re_frame/subs_test.clj @@ -1,35 +1,46 @@ (ns re-frame.subs-test (:require [clojure.test :refer :all] - [re-frame.subs :as subs] - [re-frame.db :as db])) + [re-frame.frame :as frame])) + +(def ^:dynamic *frame*) + +(defn set-frame + [f] + (binding [*frame* (frame/make-frame)] + (f))) + +(use-fixtures :once set-frame) (defn fixture-re-frame [f] - (let [restore-re-frame (re-frame.core/make-restore-fn)] + (let [restore-re-frame (frame/make-restore-fn *frame*)] (f) (restore-re-frame))) (use-fixtures :each fixture-re-frame) (deftest test-reg-sub-clj-repl - (subs/reg-sub + (frame/reg-sub + *frame* :a-sub (fn [db _] (:a db))) - (subs/reg-sub + (frame/reg-sub + *frame* :b-sub (fn [db _] (:b db))) - (subs/reg-sub + (frame/reg-sub + *frame* :a-b-sub (fn [_ _] - [(subs/subscribe [:a-sub]) - (subs/subscribe [:b-sub])]) + [(frame/subscribe *frame* [:a-sub]) + (frame/subscribe *frame* [:b-sub])]) (fn [[a b] _] {:a a :b b})) - (let [test-sub (subs/subscribe [:a-b-sub])] - (reset! db/app-db {:a 1 :b 2}) + (let [test-sub (frame/subscribe *frame* [:a-b-sub])] + (reset! (:app-db *frame*) {:a 1 :b 2}) (is (= {:a 1 :b 2} @test-sub)) - (swap! db/app-db assoc :b 3) + (swap! (:app-db *frame*) assoc :b 3) (is (= {:a 1 :b 3} @test-sub)))) diff --git a/test/re_frame/subs_test.cljs b/test/re_frame/subs_test.cljs index 8a423a8f..2040d7b5 100644 --- a/test/re_frame/subs_test.cljs +++ b/test/re_frame/subs_test.cljs @@ -5,68 +5,78 @@ [re-frame.db :as db] [re-frame.core :as re-frame])) -(test/use-fixtures :each {:before (fn [] (subs/clear-all-handlers!))}) +(defn reg-sub-raw-compat + [query-id handler-fn] + (re-frame/reg-sub-raw + query-id + (fn + ([frame query-v] + (handler-fn (:app-db frame) query-v)) + ([frame query-v dyn-v] + (handler-fn (:app-db frame) query-v dyn-v))))) + +(test/use-fixtures :each {:before (fn [] (subs/clear-all-handlers! db/default-frame))}) ;=====test basic subscriptions ====== (deftest test-reg-sub - (re-frame/reg-sub-raw + (reg-sub-raw-compat :test-sub (fn [db [_]] (reaction (deref db)))) - (let [test-sub (subs/subscribe [:test-sub])] + (let [test-sub (re-frame/subscribe [:test-sub])] (is (= @db/app-db @test-sub)) (reset! db/app-db 1) (is (= 1 @test-sub)))) (deftest test-chained-subs - (re-frame/reg-sub-raw + (reg-sub-raw-compat :a-sub (fn [db [_]] (reaction (:a @db)))) - (re-frame/reg-sub-raw + (reg-sub-raw-compat :b-sub (fn [db [_]] (reaction (:b @db)))) - (re-frame/reg-sub-raw + (reg-sub-raw-compat :a-b-sub (fn [db [_]] - (let [a (subs/subscribe [:a-sub]) - b (subs/subscribe [:b-sub])] + (let [a (re-frame/subscribe [:a-sub]) + b (re-frame/subscribe [:b-sub])] (reaction {:a @a :b @b})))) - (let [test-sub (subs/subscribe [:a-b-sub])] + (let [test-sub (re-frame/subscribe [:a-b-sub])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1 :b 2} @test-sub)) (swap! db/app-db assoc :b 3) (is (= {:a 1 :b 3} @test-sub)))) (deftest test-sub-parameters - (re-frame/reg-sub-raw + (reg-sub-raw-compat :test-sub (fn [db [_ b]] (reaction [(:a @db) b]))) - (let [test-sub (subs/subscribe [:test-sub :c])] + (let [test-sub (re-frame/subscribe [:test-sub :c])] (reset! db/app-db {:a 1 :b 2}) (is (= [1 :c] @test-sub)))) (deftest test-sub-chained-parameters - (re-frame/reg-sub-raw + (reg-sub-raw-compat :a-sub (fn [db [_ a]] (reaction [(:a @db) a]))) - (re-frame/reg-sub-raw + (reg-sub-raw-compat :b-sub (fn [db [_ b]] (reaction [(:b @db) b]))) - (re-frame/reg-sub-raw + (reg-sub-raw-compat :a-b-sub (fn [db [_ c]] - (let [a (subs/subscribe [:a-sub c]) - b (subs/subscribe [:b-sub c])] + (let [a (re-frame/subscribe [:a-sub c]) + b (re-frame/subscribe [:b-sub c])] (reaction {:a @a :b @b})))) - (let [test-sub (subs/subscribe [:a-b-sub :c])] + (let [test-sub (re-frame/subscribe [:a-b-sub :c])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a [1 :c], :b [2 :c]} @test-sub)))) @@ -79,22 +89,22 @@ (deftest test-cached-subscriptions (reset! side-effect-atom 0) - (re-frame/reg-sub-raw + (reg-sub-raw-compat :side-effecting-handler (fn side-effect [db [_] [_]] (swap! side-effect-atom inc) (reaction @db))) - (let [test-sub (subs/subscribe [:side-effecting-handler])] + (let [test-sub (re-frame/subscribe [:side-effecting-handler])] (reset! db/app-db :test) (is (= :test @test-sub)) (is (= @side-effect-atom 1)) - (subs/subscribe [:side-effecting-handler]) ;; this should be handled by cache + (re-frame/subscribe [:side-effecting-handler]) ;; this should be handled by cache (is (= @side-effect-atom 1)) - (subs/subscribe [:side-effecting-handler :a]) ;; should fire again because of the param + (re-frame/subscribe [:side-effecting-handler :a]) ;; should fire again because of the param (is (= @side-effect-atom 2)) - (subs/subscribe [:side-effecting-handler :a]) ;; this should be handled by cache + (re-frame/subscribe [:side-effecting-handler :a]) ;; this should be handled by cache (is (= @side-effect-atom 2)))) ;============== test clear-subscription-cache! ================ @@ -105,198 +115,198 @@ (fn clear-subs-cache [db _] 1)) (testing "cold cache" - (is (nil? (subs/cache-lookup [:clear-subscription-cache!])))) + (is (nil? (subs/-cache-lookup (:subs-cache db/default-frame) [:clear-subscription-cache!])))) (testing "cache miss" - (is (= 1 @(subs/subscribe [:clear-subscription-cache!]))) - (is (some? (subs/cache-lookup [:clear-subscription-cache!])))) + (is (= 1 @(re-frame/subscribe [:clear-subscription-cache!]))) + (is (some? (subs/-cache-lookup (:subs-cache db/default-frame) [:clear-subscription-cache!])))) (testing "clearing" - (subs/clear-subscription-cache!) - (is (nil? (subs/cache-lookup [:clear-subscription-cache!]))))) + (re-frame/clear-subscription-cache!) + (is (nil? (subs/-cache-lookup (:subs-cache db/default-frame) [:clear-subscription-cache!]))))) ;============== test register-pure macros ================ (deftest test-reg-sub-macro - (subs/reg-sub + (re-frame/reg-sub :test-sub (fn [db [_]] db)) - (let [test-sub (subs/subscribe [:test-sub])] + (let [test-sub (re-frame/subscribe [:test-sub])] (is (= @db/app-db @test-sub)) (reset! db/app-db 1) (is (= 1 @test-sub)))) (deftest test-reg-sub-macro-singleton - (subs/reg-sub + (re-frame/reg-sub :a-sub (fn [db [_]] (:a db))) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub (fn [_ _ _] - (subs/subscribe [:a-sub])) + (re-frame/subscribe [:a-sub])) (fn [a [_]] {:a a})) - (let [test-sub (subs/subscribe [:a-b-sub])] + (let [test-sub (re-frame/subscribe [:a-b-sub])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1} @test-sub)) (swap! db/app-db assoc :b 3) (is (= {:a 1} @test-sub)))) (deftest test-reg-sub-macro-vector - (subs/reg-sub + (re-frame/reg-sub :a-sub (fn [db [_]] (:a db))) - (subs/reg-sub + (re-frame/reg-sub :b-sub (fn [db [_]] (:b db))) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub (fn [_ _ _] - [(subs/subscribe [:a-sub]) - (subs/subscribe [:b-sub])]) + [(re-frame/subscribe [:a-sub]) + (re-frame/subscribe [:b-sub])]) (fn [[a b] [_]] {:a a :b b})) - (let [test-sub (subs/subscribe [:a-b-sub])] + (let [test-sub (re-frame/subscribe [:a-b-sub])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1 :b 2} @test-sub)) (swap! db/app-db assoc :b 3) (is (= {:a 1 :b 3} @test-sub)))) (deftest test-reg-sub-macro-map - (subs/reg-sub + (re-frame/reg-sub :a-sub (fn [db [_]] (:a db))) - (subs/reg-sub + (re-frame/reg-sub :b-sub (fn [db [_]] (:b db))) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub (fn [_ _ _] - {:a (subs/subscribe [:a-sub]) - :b (subs/subscribe [:b-sub])}) + {:a (re-frame/subscribe [:a-sub]) + :b (re-frame/subscribe [:b-sub])}) (fn [{:keys [a b]} [_]] {:a a :b b})) - (let [test-sub (subs/subscribe [:a-b-sub])] + (let [test-sub (re-frame/subscribe [:a-b-sub])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1 :b 2} @test-sub)) (swap! db/app-db assoc :b 3) (is (= {:a 1 :b 3} @test-sub)))) (deftest test-sub-macro-parameters - (subs/reg-sub + (re-frame/reg-sub :test-sub (fn [db [_ b]] [(:a db) b])) - (let [test-sub (subs/subscribe [:test-sub :c])] + (let [test-sub (re-frame/subscribe [:test-sub :c])] (reset! db/app-db {:a 1 :b 2}) (is (= [1 :c] @test-sub)))) (deftest test-sub-macros-chained-parameters - (subs/reg-sub + (re-frame/reg-sub :a-sub (fn [db [_ a]] [(:a db) a])) - (subs/reg-sub + (re-frame/reg-sub :b-sub (fn [db [_ b]] [(:b db) b])) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub (fn [[_ c] _] - [(subs/subscribe [:a-sub c]) - (subs/subscribe [:b-sub c])]) + [(re-frame/subscribe [:a-sub c]) + (re-frame/subscribe [:b-sub c])]) (fn [[a b] [_ c]] {:a a :b b})) - (let [test-sub (subs/subscribe [:a-b-sub :c])] + (let [test-sub (re-frame/subscribe [:a-b-sub :c])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a [1 :c] :b [2 :c]} @test-sub)))) (deftest test-sub-macros-<- "test the syntactical sugar" - (subs/reg-sub + (re-frame/reg-sub :a-sub (fn [db [_]] (:a db))) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub :<- [:a-sub] (fn [a [_]] {:a a})) - (let [test-sub (subs/subscribe [:a-b-sub])] + (let [test-sub (re-frame/subscribe [:a-b-sub])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1} @test-sub)))) (deftest test-sub-macros-chained-parameters-<- "test the syntactical sugar" - (subs/reg-sub + (re-frame/reg-sub :a-sub (fn [db [_]] (:a db))) - (subs/reg-sub + (re-frame/reg-sub :b-sub (fn [db [_]] (:b db))) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub :<- [:a-sub] :<- [:b-sub] (fn [[a b] [_ c]] {:a a :b b})) - (let [test-sub (subs/subscribe [:a-b-sub :c])] + (let [test-sub (re-frame/subscribe [:a-b-sub :c])] (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1 :b 2} @test-sub)))) (deftest test-sub-macros--> "test the syntactical sugar for input signal" - (subs/reg-sub + (re-frame/reg-sub :a-sub :-> :a) - (subs/reg-sub + (re-frame/reg-sub :b-sub :-> :b) - (subs/reg-sub + (re-frame/reg-sub :c-sub :-> :c) - (subs/reg-sub + (re-frame/reg-sub :d-sub :-> :d) - (subs/reg-sub + (re-frame/reg-sub :d-first-sub :<- [:d-sub] :-> first) ;; variant of :d-first-sub without an input parameter - (subs/reg-sub + (re-frame/reg-sub :e-first-sub :-> (comp first :e)) ;; test for equality - (subs/reg-sub + (re-frame/reg-sub :c-foo?-sub :<- [:c-sub] :-> #{:foo}) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub :<- [:a-sub] :<- [:b-sub] :-> (partial zipmap [:a :b])) - (let [test-sub (subs/subscribe [:a-b-sub]) - test-sub-c (subs/subscribe [:c-foo?-sub]) - test-sub-d (subs/subscribe [:d-first-sub]) - test-sub-e (subs/subscribe [:e-first-sub])] + (let [test-sub (re-frame/subscribe [:a-b-sub]) + test-sub-c (re-frame/subscribe [:c-foo?-sub]) + test-sub-d (re-frame/subscribe [:d-first-sub]) + test-sub-e (re-frame/subscribe [:e-first-sub])] (is (= nil @test-sub-c)) (reset! db/app-db {:a 1 :b 2 :c :foo :d [1 2] :e [3 4]}) (is (= {:a 1 :b 2} @test-sub)) @@ -306,55 +316,55 @@ (deftest test-sub-macros-=> "test the syntactical sugar for input signals and query vector arguments" - (subs/reg-sub + (re-frame/reg-sub :a-sub :-> :a) - (subs/reg-sub + (re-frame/reg-sub :b-sub :-> :b) - (subs/reg-sub + (re-frame/reg-sub :test-a-sub :<- [:a-sub] :=> vector) ;; test for equality of input signal and query parameter - (subs/reg-sub + (re-frame/reg-sub :test-b-sub :<- [:b-sub] :=> =) - (let [test-a-sub (subs/subscribe [:test-a-sub :c]) - test-b-sub (subs/subscribe [:test-b-sub 2])] + (let [test-a-sub (re-frame/subscribe [:test-a-sub :c]) + test-b-sub (re-frame/subscribe [:test-b-sub 2])] (reset! db/app-db {:a 1 :b 2}) (is (= [1 :c] @test-a-sub)) (is (= true @test-b-sub)))) (deftest test-registering-subs-doesnt-create-subscription (let [sub-called? (atom false)] - (with-redefs [subs/subscribe (fn [& args] (reset! sub-called? true))] - (subs/reg-sub + (with-redefs [re-frame/subscribe (fn [& args] (reset! sub-called? true))] + (re-frame/reg-sub :a-sub (fn [db [_]] (:a db))) - (subs/reg-sub + (re-frame/reg-sub :b-sub (fn [db [_]] (:b db))) - (subs/reg-sub + (re-frame/reg-sub :fn-sub (fn [[_ c] _] - [(subs/subscribe [:a-sub c]) - (subs/subscribe [:b-sub c])]) + [(re-frame/subscribe [:a-sub c]) + (re-frame/subscribe [:b-sub c])]) (fn [db [_]] (:b db))) - (subs/reg-sub + (re-frame/reg-sub :a-sugar-sub :<- [:a-sub] (fn [[a] [_ c]] {:a a})) - (subs/reg-sub + (re-frame/reg-sub :a-b-sub :<- [:a-sub] :<- [:b-sub] @@ -365,14 +375,14 @@ ;; Dynamic subscriptions (deftest test-dynamic-subscriptions - (subs/reg-sub + (re-frame/reg-sub :dyn-sub (fn [db ev dynv] (first dynv))) (testing "happy case" - (is (= 1 @(subs/subscribe [:dyn-sub] [(r/atom 1)])))) + (is (= 1 @(re-frame/subscribe [:dyn-sub] [(r/atom 1)])))) (testing "subscription that doesn't exist" - (is (nil? (subs/subscribe [:non-existent] [(r/atom 1)])))) + (is (nil? (re-frame/subscribe [:non-existent] [(r/atom 1)])))) (testing "Passing a non-reactive value to a dynamic subscription" - (is (thrown-with-msg? js/Error #"No protocol method IDeref" @(subs/subscribe [:dyn-sub] [1]))))) + (is (thrown-with-msg? js/Error #"No protocol method IDeref" @(re-frame/subscribe [:dyn-sub] [1])))))