Skip to content
This repository has been archived by the owner on Oct 28, 2019. It is now read-only.

Commit

Permalink
Merge pull request #22 from pairshaped/query-is-equal
Browse files Browse the repository at this point in the history
Implement {Query,Reference}.isEqual(other)
  • Loading branch information
rapind authored Apr 7, 2017
2 parents 6cf645e + 3c313d9 commit 29cbf61
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 136 deletions.
56 changes: 8 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ It is completely possible that master will be broken at any given time until we

## Elm guarantees

In it's current state, elm-firebase completely removes any runtime guarantees that Elm provides. This is because firebase is a close-source black box, which makes it very untestable. When you use this library, you are risking, as [@rtfeldman](https://github.com/rtfeldman) put it; "wrapping a JS library where you can't even know how it works is just bound to cost you hours of debugging down the line".

In it's current state, **elm-firebase completely removes any runtime guarantees that Elm provides**. This is because firebase is a close-source black box, full of mystery and wonder, which makes it very untestable. When you use this library, you are risking, as [@rtfeldman](https://github.com/rtfeldman) put it; "wrapping a JS library where you can't even know how it works is just bound to cost you hours of debugging down the line".

With that in mind, feel free to play with this, but use it at your own risk.

Expand Down Expand Up @@ -37,7 +36,7 @@ Then you can add elm-firebase to your elm-package.json like so:
```
{
"dependencies": {
"pairshaped/elm-firebase": "0.0.11 <= v < 1.0.0"
"pairshaped/elm-firebase": "0.0.12 <= v < 1.0.0"
},
"dependency-sources": {
"pairshaped/elm-firebase": {
Expand All @@ -64,6 +63,7 @@ Here are a list of firebase versions that have or will be tested:
|---------|----------|----------|
| 3.6.9 | YES | https://www.gstatic.com/firebasejs/3.6.9/firebase.js |
| 3.7.1 | Probably | https://www.gstatic.com/firebasejs/3.7.1/firebase.js |
| 3.7.4 | YES | https://www.gstatic.com/firebasejs/3.7.4/firebase.js |

If you run into a weird or unexplainable bug, please ensure you are using a version that has been tested and verified.

Expand All @@ -74,46 +74,6 @@ I expect all the 3.x firebase versions to work but sticking to known versions wi
- `snapshot.val()` maps to `Firebase.Database.Snapshot.value snapshot` rather than `Firebase.Database.Snapshot.val snapshot`. I chose to be more explicit because I thought `val` wasn't as meaningful as it could be.
- `reference.on`, `reference.off`, `query.on`, and `query.off` map to singular subscription methods: `Firebase.Database.Reference.on` and `Firebase.Database.Query.on` respectively.
When you're done, just remove your subscription from `Sub.batch` and elm-firebase will do the rest!
- Subscribed queries must live on the model, see [Internals of a Subscription](#Internals-of-a-Subscription) for more information.

### Interals of a Subscription

Building queries from references in Elm also assigns a hidden uuid.
At the time of this writing, Elm does not support comparing tagger methods, and firebase doesn't provide a way to identify how a query was created (ie ordering, comparisons, etc.).
This is a huge problem for subscribing to a query, because Elm needs a way to know if a subscription is new (begin the subscription in js), persisting (do nothing, let the subscription continue), or removed (tell js to stop the subscription).
The hidden uuid will always guarantee uniqueness, but at a small price - you *must* keep the queries you're interested using inside your model (or some persistent storage between updates).
Every time you create or modify a query, the uuid is re-generated.

To demonstrated:

```
sourceRef : Firebase.Database.Types.Reference
sourceRef =
app
|> Firebase.Database.init
|> Firebase.Database.ref (Just "foo")
queryOne : Firebase.Database.Types.Query
queryOne =
sourceRef
|> Firebase.Database.Reference.orderByValue
|> Firebase.Database.Query.limitToFirst 1
queryTwo : Firebase.Database.Types.Query
queryTwo =
sourceRef
|> Firebase.Database.Reference.orderByValue
|> Firebase.Database.Query.limitToFirst 1
```

Even though `queryOne` and `queryTwo` will produce the same results when subscribed to, they are technically not the same to the query effect manager.

I may change this in the future so that queries keep track of how they were built.
The downside of this is that it would be the burden of native code to track this, but would mean we can optimize the number of active subscriptions by having multiple taggers re-use the same subscription if they are the same.
The only main change this would provide is removing the requirement for a query to be in the model.
I'm personally not convinced that the query in the model is a bad thing, since the `subscriptions` method should probably be as dumb as possible.

## Connecting to your firebase database

Expand Down Expand Up @@ -146,12 +106,12 @@ init =
app : Firebase.App
app =
Firebase.init
{ apiKey: "your firebase api key"
, databaseURL: "https://your-firebase-app.firebaseio.com"
{ apiKey = "your firebase api key"
, databaseURL = "https://your-firebase-app.firebaseio.com"
, -- These are necessary for just connecting to your database
authDomain: ""
, storageBucket: ""
, messagingSenderId: ""
authDomain = ""
, storageBucket = ""
, messagingSenderId = ""
}
{-
Expand Down
2 changes: 1 addition & 1 deletion examples/kitchenSink/public/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<script src="https://www.gstatic.com/firebasejs/3.6.9/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.7.4/firebase.js"></script>
<script type="text/javascript" src="./main.js"></script>
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion examples/writer/public/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<script src="https://www.gstatic.com/firebasejs/3.6.9/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.7.4/firebase.js"></script>
<script type="text/javascript" src="./main.js"></script>
</head>
<body>
Expand Down
150 changes: 135 additions & 15 deletions src/Firebase/Database/Query.elm
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,185 @@ effect module Firebase.Database.Query
, limitToLast
, once
, on
, isEqual
)

{-| Firebase Database Queries
Queries are always built off `Firebase.Database.Reference` calls. See `Firebase.Database.Reference.orderBy*` methods to see how to kick off a query.
# Utility
@docs ref, isEqual
# Query qualifiers
@docs startAt, endAt, equalTo, limitToFirst, limitToLast
# Query execution
@docs once, on
For more information on all of these functions, see [the firebase docs](https://firebase.google.com/docs/reference/js/firebase.database.Query)
-}

import Json.Encode
import Task exposing (Task)
import Firebase.Database.Types exposing (Query, Reference, Snapshot)
import Native.Database.Query


{-| Given a query, return it's base reference
myRef : Firebase.Database.Types.Reference
myRef =
myDatabase
|> Firebase.Database.ref (Just "foo")
|> Firebase.Database.child "bar"
myQuery : Firebase.Database.Types.Query
myQuery =
myRef
|> Firebase.Database.Reference.orderByKey
myQueryRef : Firebase.Database.Types.Reference
myQueryRef =
Firebase.Database.Query.ref myQuery
Firebase.Database.Reference.isEqual myRef myQueryRef
-- => True
See [firebase.database.Query#ref](https://firebase.google.com/docs/reference/js/firebase.database.Query#ref) for more information
-}
ref : Query -> Reference
ref =
Native.Database.Query.ref


{-| Given two queries, detect if they are the same.
According to the firebase documentation, "Two `Query` objects are equivalent if they represent the same location, have the same query parameters, and are from the same instance of `firebase.app.App`. Equivalent queries share the same sort order, limits, and starting and ending points."
See [firebase.database.Query#isEqual](https://firebase.google.com/docs/reference/js/firebase.database.Query#isEqual)
-}
isEqual : Query -> Query -> Bool
isEqual =
Native.Database.Query.isEqual


{-| Given a Json value, optional string key, and query, specify the start point of a query.
Start at specifies the first value (is inclusive). If key is specified, you can decrease the total scope of the search.
The behaviour of startAt changes based on the ordering specified from the reference:
- orderByChild: value represents the value of the specified child
- orderByKey: value represents the the key
- orderByValue: value represents the first instance of the given value
- orderByPriority: value represents the priority (either a float or string). In all likelihood, you won't use this one.
If you're not using the optional key, pass a `Nothing` in its place.
See [firebase.database.Query#startAt](https://firebase.google.com/docs/reference/js/firebase.database.Query#startAt)
-}
startAt : Json.Encode.Value -> Maybe String -> Query -> Query
startAt =
Native.Database.Query.startAt


{-| Given a Json value, optional string key, and query, specify the end point of a query.
End at specifies the last value (is inclusive). If key is specified, you can decrease the total scope of the search.
The behaviour of endAt changes based on the ordering specified from the reference:
- orderByChild: value represents the value of the specified child
- orderByKey: value represents the the key
- orderByValue: value represents the last instance of the given value
- orderByPriority: value represents the priority (either a float or string). In all likelihood, you won't use this one.
If you're not using the optional key, pass a `Nothing` in its place.
See [firebase.database.Query#endAt](https://firebase.google.com/docs/reference/js/firebase.database.Query#endAt)
-}
endAt : Json.Encode.Value -> Maybe String -> Query -> Query
endAt =
Native.Database.Query.endAt


{-| Given a Json value, optional string key, and query, specify the target value of a query.
Equal to specifies the exact value. If key is specified, you can decrease the total scope of the search.
The behaviour of equalTo changes based on the ordering specified from the reference:
- orderByChild: value represents the value of the specified child
- orderByKey: value represents the the key
- orderByValue: value represents all instances of the given value
- orderByPriority: value represents the priority (either a float or string). In all likelihood, you won't use this one.
If you're not using the optional key, pass a `Nothing` in its place.
See [firebase.database.Query#equalTo](https://firebase.google.com/docs/reference/js/firebase.database.Query#equalTo)
-}
equalTo : Json.Encode.Value -> Maybe String -> Query -> Query
equalTo =
Native.Database.Query.equalTo


{-| Given an integer limitation and query, specify to use only the first n objects from the resulting query.
See [firebase.database.Query#limitToFirst](https://firebase.google.com/docs/reference/js/firebase.database.Query#limitToFirst)
-}
limitToFirst : Int -> Query -> Query
limitToFirst =
Native.Database.Query.limitToFirst


{-| Given an integer limitation and query, specify to use only the last n objects from the resulting query.
See [firebase.database.Query#limitToLast](https://firebase.google.com/docs/reference/js/firebase.database.Query#limitToLast)
-}
limitToLast : Int -> Query -> Query
limitToLast =
Native.Database.Query.limitToLast


{-| Given an event type and query, return a task to capture the result of the query.
Event types include:
- `"value"` - get the value of the objects matching the query.
- `"child_added"` - get the first new child in the current query.
- `"child_changed"` - get the first modified child in the current query.
- `"child_removed"` - get the first child modified to by null in the current query.
- `"child_moved"` - get the object where the key has changed in the current query.
Most likely, with one-off queries using `.once`, you'll be using `"value"`.
See [firebase.database.Query#once](https://firebase.google.com/docs/reference/js/firebase.database.Query#once)
-}
once : String -> Query -> Task x Snapshot
once =
Native.Database.Query.once


on : String -> Query -> (Snapshot -> msg) -> Sub msg
on event query tagger =
subscription (MySub event query tagger)


{-| Given an event type and query, return a subscription to this query
-- Private
Even types include:
- `"value"` - watch all values matching this query
- `"child_added"` - watch for all new children added matching this query
- `"child_changed"` - watch for all modified children matching this query
- `"child_removed"` - watch for all children that stop matching this query or are deleted
- `"child_moved"` - watch for all children with modified keys matching this query
uuid : Query -> String
uuid =
Native.Database.Query.uuid
See [firebase.database.Query#once](https://firebase.google.com/docs/reference/js/firebase.database.Query#once)
-}
on : String -> Query -> (Snapshot -> msg) -> Sub msg
on event query tagger =
subscription (MySub event query tagger)



Expand Down Expand Up @@ -121,16 +245,12 @@ onEffects :
-> Task never (State msg)
onEffects router newSubs oldState =
let
isSameQuery : Query -> Query -> Bool
isSameQuery a b =
(uuid a) == (uuid b)

toAdd : List (MySub msg)
toAdd =
let
notSubscribed (MySub newEvent newQuery _) =
oldState.subs
|> List.filter (\(MySub event query _) -> event == newEvent && (isSameQuery query newQuery))
|> List.filter (\(MySub event query _) -> event == newEvent && (isEqual query newQuery))
|> List.isEmpty
in
newSubs
Expand All @@ -141,7 +261,7 @@ onEffects router newSubs oldState =
let
subscribed (MySub oldEvent oldQuery _) =
newSubs
|> List.filter (\(MySub event query _) -> event == oldEvent && (isSameQuery query oldQuery))
|> List.filter (\(MySub event query _) -> event == oldEvent && (isEqual query oldQuery))
|> List.isEmpty
in
oldState.subs
Expand Down
10 changes: 8 additions & 2 deletions src/Firebase/Database/Reference.elm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ effect module Firebase.Database.Reference
, toString
, once
, on
, isEqual
)

import Json.Encode
Expand Down Expand Up @@ -95,6 +96,11 @@ onDisconnect =
Native.Database.Reference.onDisconnect


isEqual : Reference -> Reference -> Bool
isEqual =
Native.Database.Reference.isEqual


once : String -> Reference -> Task x Snapshot
once =
Native.Database.Reference.once
Expand Down Expand Up @@ -165,7 +171,7 @@ onEffects router newSubs oldState =
let
notSubscribed (MySub newEvent newReference _) =
oldState.subs
|> List.filter (\(MySub event reference _) -> event == newEvent && (toString reference) == (toString newReference))
|> List.filter (\(MySub event reference _) -> event == newEvent && (isEqual reference newReference))
|> List.isEmpty
in
newSubs
Expand All @@ -176,7 +182,7 @@ onEffects router newSubs oldState =
let
subscribed (MySub oldEvent oldReference tagger) =
newSubs
|> List.filter (\(MySub event reference _) -> event == oldEvent && (toString reference) == (toString oldReference))
|> List.filter (\(MySub event reference _) -> event == oldEvent && (isEqual reference oldReference))
|> List.isEmpty
in
oldState.subs
Expand Down
Loading

0 comments on commit 29cbf61

Please sign in to comment.