From 75f579245b68cad0724a05c6568286744145b309 Mon Sep 17 00:00:00 2001
From: Haydn Ewers
Date: Thu, 24 Sep 2015 17:51:40 +0930
Subject: [PATCH 1/4] Use RxJS for handling events.
---
package.json | 3 +-
spec/shared.js | 1 +
spec/store/events/observable-spec.js | 72 +++++++++++++++++++++++
src/store.js | 86 +++++++++++++---------------
4 files changed, 114 insertions(+), 48 deletions(-)
create mode 100644 spec/store/events/observable-spec.js
diff --git a/package.json b/package.json
index c8a484d..f40c764 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"uglifyify": "^3.0.1"
},
"dependencies": {
- "array.prototype.find": "^1.0.0"
+ "array.prototype.find": "^1.0.0",
+ "rx-lite": "^3.1.2"
}
}
diff --git a/spec/shared.js b/spec/shared.js
index 564c15c..5f3ea86 100644
--- a/spec/shared.js
+++ b/spec/shared.js
@@ -8,6 +8,7 @@ import "./store/core/define-spec";
import "./store/core/find-spec";
import "./store/core/push-spec";
import "./store/core/remove-spec";
+import "./store/events/observable-spec";
import "./store/events/off-spec";
import "./store/events/on-spec";
import "./store/fields/attr-spec";
diff --git a/spec/store/events/observable-spec.js b/spec/store/events/observable-spec.js
new file mode 100644
index 0000000..6013aca
--- /dev/null
+++ b/spec/store/events/observable-spec.js
@@ -0,0 +1,72 @@
+import test from "tape-catch";
+import sinon from "sinon";
+import Store from "../../../src/store";
+
+test("observable must fire an added event when a resource is added to the store", function (t) {
+ var store = new Store();
+ var listener = sinon.spy();
+ t.plan(5);
+ store.define("products", {});
+ store.observable.subscribe(listener);
+ store.add({
+ "type": "products",
+ "id": "1"
+ });
+ t.equal(listener.callCount, 1);
+ t.equal(listener.firstCall.args[0].event, "added");
+ t.equal(listener.firstCall.args[0].type, "products");
+ t.equal(listener.firstCall.args[0].id, "1");
+ t.equal(listener.firstCall.args[0].value, store.find("products", "1"));
+});
+
+test("observable must fire an updated event when a resource is update in the store", function (t) {
+ var store = new Store();
+ var listener = sinon.spy();
+ t.plan(5);
+ store.define("products", {
+ title: Store.attr()
+ });
+ store.add({
+ "type": "products",
+ "id": "1",
+ "attributes": {
+ "title": "foo"
+ }
+ });
+ store.observable.subscribe(listener);
+ store.add({
+ "type": "products",
+ "id": "1",
+ "attributes": {
+ "title": "bar"
+ }
+ });
+ t.equal(listener.callCount, 1);
+ t.equal(listener.firstCall.args[0].event, "updated");
+ t.equal(listener.firstCall.args[0].type, "products");
+ t.equal(listener.firstCall.args[0].id, "1");
+ t.equal(listener.firstCall.args[0].value, store.find("products", "1"));
+});
+
+test("observable must fire a removed event when a resource is removed from the store", function (t) {
+ var store = new Store();
+ var listener = sinon.spy();
+ t.plan(5);
+ store.define("products", {
+ title: Store.attr()
+ });
+ store.add({
+ "type": "products",
+ "id": "1",
+ "attributes": {
+ "title": "foo"
+ }
+ });
+ store.observable.subscribe(listener);
+ store.remove("products", "1");
+ t.equal(listener.callCount, 1);
+ t.equal(listener.firstCall.args[0].event, "removed");
+ t.equal(listener.firstCall.args[0].type, "products");
+ t.equal(listener.firstCall.args[0].id, "1");
+ t.equal(listener.firstCall.args[0].value, null);
+});
diff --git a/src/store.js b/src/store.js
index 3846a57..cd2ff01 100644
--- a/src/store.js
+++ b/src/store.js
@@ -1,4 +1,5 @@
import "array.prototype.find";
+import Rx from "rx-lite";
import AjaxAdapter from "./ajax-adapter";
export default class Store {
@@ -93,11 +94,15 @@ export default class Store {
}
constructor(adapter) {
+
this._adapter = adapter;
- this._collectionListeners = { "added": {}, "updated": {}, "removed": {} };
this._data = {};
- this._resourceListeners = { "added": {}, "updated": {}, "removed": {} };
+ this._subject = new Rx.Subject();
+ this._subscriptions = {};
this._types = {};
+
+ this.observable = this._subject.asObservable();
+
}
/**
@@ -120,12 +125,12 @@ export default class Store {
this._addField(object, resource, definition, fieldName);
}
});
- if (this._resourceListeners[event][object.type] && this._resourceListeners[event][object.type][object.id]) {
- this._resourceListeners[event][object.type][object.id].forEach(x => x[0].call(x[1], resource));
- }
- if (this._collectionListeners[event][object.type]) {
- this._collectionListeners[event][object.type].forEach(x => x[0].call(x[1], resource));
- }
+ this._subject.onNext({
+ event: event,
+ type: object.type,
+ id: object.id,
+ value: resource
+ });
} else {
throw new TypeError(`The data must have a type and id`);
}
@@ -329,25 +334,13 @@ export default class Store {
* @return {undefined} - Nothing.
*/
off(event, type, id, callback) {
- if (this._resourceListeners[event] && this._collectionListeners[event]) {
+ if (event === "added" || event === "updated" || event === "removed") {
if (this._types[type]) {
if (id && ({}).toString.call(id) === '[object Function]') {
this.off.call(this, event, type, null, id, callback);
- } else {
- // TODO: Performance-wise, this can be made way better. There shouldn't be a need to maintain separate lists.
- this._types[type]._names.forEach(type => {
- if (id) {
- if (this._resourceListeners[event][type] && this._resourceListeners[event][type][id]) {
- this._resourceListeners[event][type][id] = this._resourceListeners[event][type][id].filter(x => {
- return x[0] !== callback;
- });
- }
- } else if (this._collectionListeners[event][type]) {
- this._collectionListeners[event][type] = this._collectionListeners[event][type].filter(x => {
- return x[0] !== callback;
- });
- }
- });
+ } else if (this._subscriptions[event] && this._subscriptions[event][type] && this._subscriptions[event][type][id || "*"]) {
+ this._subscriptions[event][type][id || "*"].dispose();
+ delete this._subscriptions[event][type][id || "*"];
}
} else {
throw new Error(`Unknown type '${type}'`);
@@ -369,26 +362,25 @@ export default class Store {
* @return {undefined} - Nothing.
*/
on(event, type, id, callback, context) {
- if (this._resourceListeners[event] && this._collectionListeners[event]) {
+ if (event === "added" || event === "updated" || event === "removed") {
if (this._types[type]) {
if (id && ({}).toString.call(id) === '[object Function]') {
this.on.call(this, event, type, null, id, callback);
- } else {
- // TODO: Performance-wise, this can be made way better. There shouldn't be a need to maintain separate lists.
- this._types[type]._names.forEach(type => {
- if (id) {
- this._resourceListeners[event][type] = this._resourceListeners[event][type] || {};
- this._resourceListeners[event][type][id] = this._resourceListeners[event][type][id] || [];
- if (!this._resourceListeners[event][type][id].find(x => x[0] === callback)) {
- this._resourceListeners[event][type][id].push([ callback, context ]);
- }
- } else {
- this._collectionListeners[event][type] = this._collectionListeners[event][type] || [];
- if (!this._collectionListeners[event][type].find(x => x[0] === callback)) {
- this._collectionListeners[event][type].push([ callback, context ]);
- }
- }
- });
+ } else if (!this._subscriptions[event] || !this._subscriptions[event][type] || !this._subscriptions[event][type][id || "*"]) {
+ let subscription = this._subject.filter(e => e.event === event);
+ subscription = subscription.filter(e => this._types[type]._names.indexOf(e.type) !== -1);
+ if (id) {
+ subscription = subscription.filter(e => e.id === id);
+ }
+ subscription = subscription.map(e => this.find(e.type, e.id));
+ this._subscriptions[event] = this._subscriptions[event] || {};
+ if (!this._subscriptions[event][type]) {
+ let obj = {};
+ this._types[type]._names.forEach(type => {
+ this._subscriptions[event][type] = obj;
+ });
+ }
+ this._subscriptions[event][type][id || "*"] = subscription.subscribe(callback.bind(context));
}
} else {
throw new Error(`Unknown type '${type}'`);
@@ -434,12 +426,12 @@ export default class Store {
let resource = this._data[type] && this._data[type][id];
if (resource) {
this._remove(resource);
- if (this._resourceListeners["removed"][type] && this._resourceListeners["removed"][type][id]) {
- this._resourceListeners["removed"][type][id].forEach(x => x[0].call(x[1], resource));
- }
- if (this._collectionListeners["removed"][type]) {
- this._collectionListeners["removed"][type].forEach(x => x[0].call(x[1], resource));
- }
+ this._subject.onNext({
+ event: "removed",
+ type: type,
+ id: id,
+ value: null
+ });
}
} else {
Object.keys(this._data[type]).forEach(id => this.remove(type, id));
--
2.51.2
From f4bec1df45206ad68e7a1692e01e0f8de4dc1b5e Mon Sep 17 00:00:00 2001
From: Haydn Ewers
Date: Thu, 1 Oct 2015 16:56:13 +0930
Subject: [PATCH 2/4] Improve docs and consistency in the events API.
---
README.md | 7 ++--
package.json | 2 +-
spec/store/events/observable-spec.js | 12 +++----
src/store.js | 49 ++++++++++++++++++++++++----
4 files changed, 54 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index b4e9ef9..f6b21a8 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
# JSON API Store [](https://travis-ci.org/haydn/json-api-store) [](http://badge.fury.io/js/json-api-store)
-A lightweight JavaScript library that acts as a data store and uses the
-[JSON API](http://jsonapi.org) specification. You can use it in the browser to
-easily communicate with a JSON API compliant server.
+An isomorphic JavaScript library that acts as an in memory data store for
+[JSON API](http://jsonapi.org) data. Changes are
+broadcast using [RxJS](https://github.com/Reactive-Extensions/RxJS). Built to
+work with [React](https://facebook.github.io/react/).
## Usage
diff --git a/package.json b/package.json
index f40c764..f7ee542 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "json-api-store",
"version": "0.5.1",
- "description": "A lightweight library for using JSON API in the browser.",
+ "description": "An isomorphic JavaScript library that acts as an in memory data store for JSON API data.",
"repository": "haydn/json-api-store",
"main": "dist/store.js",
"scripts": {
diff --git a/spec/store/events/observable-spec.js b/spec/store/events/observable-spec.js
index 6013aca..530ded3 100644
--- a/spec/store/events/observable-spec.js
+++ b/spec/store/events/observable-spec.js
@@ -13,10 +13,10 @@ test("observable must fire an added event when a resource is added to the store"
"id": "1"
});
t.equal(listener.callCount, 1);
- t.equal(listener.firstCall.args[0].event, "added");
+ t.equal(listener.firstCall.args[0].name, "added");
t.equal(listener.firstCall.args[0].type, "products");
t.equal(listener.firstCall.args[0].id, "1");
- t.equal(listener.firstCall.args[0].value, store.find("products", "1"));
+ t.equal(listener.firstCall.args[0].resource, store.find("products", "1"));
});
test("observable must fire an updated event when a resource is update in the store", function (t) {
@@ -42,10 +42,10 @@ test("observable must fire an updated event when a resource is update in the sto
}
});
t.equal(listener.callCount, 1);
- t.equal(listener.firstCall.args[0].event, "updated");
+ t.equal(listener.firstCall.args[0].name, "updated");
t.equal(listener.firstCall.args[0].type, "products");
t.equal(listener.firstCall.args[0].id, "1");
- t.equal(listener.firstCall.args[0].value, store.find("products", "1"));
+ t.equal(listener.firstCall.args[0].resource, store.find("products", "1"));
});
test("observable must fire a removed event when a resource is removed from the store", function (t) {
@@ -65,8 +65,8 @@ test("observable must fire a removed event when a resource is removed from the s
store.observable.subscribe(listener);
store.remove("products", "1");
t.equal(listener.callCount, 1);
- t.equal(listener.firstCall.args[0].event, "removed");
+ t.equal(listener.firstCall.args[0].name, "removed");
t.equal(listener.firstCall.args[0].type, "products");
t.equal(listener.firstCall.args[0].id, "1");
- t.equal(listener.firstCall.args[0].value, null);
+ t.equal(listener.firstCall.args[0].resource, null);
});
diff --git a/src/store.js b/src/store.js
index cd2ff01..07b1732 100644
--- a/src/store.js
+++ b/src/store.js
@@ -101,6 +101,43 @@ export default class Store {
this._subscriptions = {};
this._types = {};
+ /**
+ * An observable that will emit events when any resource in added, updated
+ * or removed. The object passed to listeners will be in this format:
+ *
+ *
+ * { name: string, type: string, id: string, resource: object }
+ *
+ *
+ * You can learn more about RxJS observables at the GitHub repo:
+ * https://github.com/Reactive-Extensions/RxJS
+ *
+ * @type {Rx.Observable}
+ *
+ * @example
+ * let store = new Store();
+ *
+ * store.observable.filter(e => e.name === "added").subscribe(event => {
+ * console.log(event.name); // "added"
+ * console.log(event.type); // "products"
+ * console.log(event.id); // "1"
+ * console.log(event.resource); // Map {...}
+ * });
+ *
+ * store.observable.filter(e => e.name === "updated").subscribe(event => {
+ * console.log(event.name); // "updated"
+ * console.log(event.type); // "products"
+ * console.log(event.id); // "1"
+ * console.log(event.resource); // Map {...}
+ * });
+ *
+ * store.observable.filter(e => e.name === "removed").subscribe(event => {
+ * console.log(event.name); // "removed"
+ * console.log(event.type); // "products"
+ * console.log(event.id); // "1"
+ * console.log(event.resource); // null
+ * });
+ */
this.observable = this._subject.asObservable();
}
@@ -117,7 +154,7 @@ export default class Store {
add(object) {
if (object) {
if (object.type && object.id) {
- let event = this._data[object.type] && this._data[object.type][object.id] ? "updated" : "added";
+ let name = this._data[object.type] && this._data[object.type][object.id] ? "updated" : "added";
let resource = this.find(object.type, object.id);
let definition = this._types[object.type];
Object.keys(definition).forEach(fieldName => {
@@ -126,10 +163,10 @@ export default class Store {
}
});
this._subject.onNext({
- event: event,
+ name: name,
type: object.type,
id: object.id,
- value: resource
+ resource: resource
});
} else {
throw new TypeError(`The data must have a type and id`);
@@ -367,7 +404,7 @@ export default class Store {
if (id && ({}).toString.call(id) === '[object Function]') {
this.on.call(this, event, type, null, id, callback);
} else if (!this._subscriptions[event] || !this._subscriptions[event][type] || !this._subscriptions[event][type][id || "*"]) {
- let subscription = this._subject.filter(e => e.event === event);
+ let subscription = this._subject.filter(e => e.name === event);
subscription = subscription.filter(e => this._types[type]._names.indexOf(e.type) !== -1);
if (id) {
subscription = subscription.filter(e => e.id === id);
@@ -427,10 +464,10 @@ export default class Store {
if (resource) {
this._remove(resource);
this._subject.onNext({
- event: "removed",
+ name: "removed",
type: type,
id: id,
- value: null
+ resource: null
});
}
} else {
--
2.51.2
From c4716f9980ebdc03c5ec7ae8d4c3910e20170cb8 Mon Sep 17 00:00:00 2001
From: Haydn Ewers
Date: Fri, 2 Oct 2015 13:05:07 +0930
Subject: [PATCH 3/4] Add bind() polyfill to the tests.
---
spec/shared.js | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/spec/shared.js b/spec/shared.js
index 5f3ea86..cdc5835 100644
--- a/spec/shared.js
+++ b/spec/shared.js
@@ -1,3 +1,31 @@
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function(oThis) {
+ if (typeof this !== 'function') {
+ // closest thing possible to the ECMAScript 5
+ // internal IsCallable function
+ throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+ }
+
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function() {},
+ fBound = function() {
+ return fToBind.apply(this instanceof fNOP
+ ? this
+ : oThis,
+ aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ if (this.prototype) {
+ // native functions don't have a prototype
+ fNOP.prototype = this.prototype;
+ }
+ fBound.prototype = new fNOP();
+
+ return fBound;
+ };
+}
+
import "./store/clud/create-spec";
import "./store/clud/destroy-spec";
import "./store/clud/load-spec";
--
2.51.2
From ea0bd1c05762d35f1289a49325cdcf8266b57437 Mon Sep 17 00:00:00 2001
From: Haydn Ewers
Date: Fri, 2 Oct 2015 14:07:41 +0930
Subject: [PATCH 4/4] Improve documentation.
---
README.md | 27 ++++++++++++++++-----------
src/.eslintrc | 1 -
src/store.js | 15 ++++-----------
3 files changed, 20 insertions(+), 23 deletions(-)
diff --git a/README.md b/README.md
index f6b21a8..69285dc 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ At the moment the primary use can for JSON API Store is in the browser:
```javascript
// Create a new store instance.
-var adapter = new Store.AjaxAdapter({ base: "http://example.com/api/v1" });
+var adapter = new Store.AjaxAdapter({ base: "/api/v1" });
var store = new Store(adapter);
// Define the "categories" type.
@@ -29,6 +29,11 @@ store.define([ "products", "product" ], {
category: Store.hasOne()
});
+// Subscribe to events using RxJS.
+store.observable.subscribe(function (event) {
+ console.log(event.name, event.type, event.id, event.resource);
+});
+
// Load all the products.
store.load("products", { include: [ "category" ] }, function (products) {
@@ -47,11 +52,11 @@ store.load("products", { include: [ "category" ] }, function (products) {
### Node
-You can also use JSON API Store in a Node.js environment (adapters that work in
-a Node.js are in the works):
+You can also use JSON API Store in a Node.js environment (currently, there
+aren't any adapters that work in a Node.js):
-**NOTE**: Without an adapter the `create`, `load`, `update` and `destroy`
-methods cannot be used.
+**NOTE**: Without an adapter the CLUD methods (`create`, `load`, `update` and
+`destroy`) cannot be used.
```javascript
@@ -97,6 +102,12 @@ store.find("products", "1").category.title; // "Example Category"
```
+## Documentation
+
+Full documentation is available on the website:
+
+http://particlesystem.com/json-api-store/
+
## Installing
#### NPM
@@ -114,9 +125,3 @@ bower i json-api-store
#### Download
To use directly in the browser you can grab the [store.prod.js](https://raw.githubusercontent.com/haydn/json-api-store/master/dist/store.prod.js) file.
-
-## Documentation
-
-Documentation is available on the website:
-
-http://particlesystem.com/json-api-store/
diff --git a/src/.eslintrc b/src/.eslintrc
index 03aab31..c02382a 100644
--- a/src/.eslintrc
+++ b/src/.eslintrc
@@ -57,7 +57,6 @@
"no-with": 2,
"radix": 2,
"semi": 2,
- "valid-jsdoc": 2,
"vars-on-top": 2,
"wrap-iife": 2,
"yoda": 2
diff --git a/src/store.js b/src/store.js
index 1de2295..0508cf9 100644
--- a/src/store.js
+++ b/src/store.js
@@ -134,6 +134,7 @@ export default class Store {
* https://github.com/Reactive-Extensions/RxJS
*
* @type {Rx.Observable}
+ * @since 0.6.0
*
* @example
* let store = new Store();
@@ -168,9 +169,8 @@ export default class Store {
* `push()` method.
*
* @since 0.1.0
- * @param {!Object} object - Resource Object to add. See:
+ * @param {!Object} object - A JSON API Resource Object to be added. See:
http://jsonapi.org/format/#document-resource-objects
- * @return {undefined} - Nothing.
*/
add(object) {
if (object) {
@@ -240,7 +240,6 @@ export default class Store {
* @param {function} [success] - Callback on success.
* @param {function} [error] - Callback on error.
* @param {Object} [context] - Context for the callbacks.
- * @return {undefined} - Nothing.
*
* @example
* let adapter = new Store.AjaxAdapter();
@@ -263,7 +262,6 @@ export default class Store {
* @since 0.2.0
* @param {!string|string[]} names - Name(s) of the resource.
* @param {!Object} definition - The resource's definition.
- * @return {undefined} - Nothing.
*/
define(names, definition) {
names = (names.constructor === Array) ? names : [ names ];
@@ -291,7 +289,6 @@ export default class Store {
* @param {function} [success] - Callback on success.
* @param {function} [error] - Callback on error.
* @param {Object} [context] - Context for the callbacks.
- * @return {undefined} - Nothing.
*
* @example
* let adapter = new Store.AjaxAdapter();
@@ -364,7 +361,6 @@ export default class Store {
* @param {function} [success] - Callback on success.
* @param {function} [error] - Callback on error.
* @param {Object} [context] - Context for the callbacks.
- * @return {undefined} - Nothing.
*
* @example
* let adapter = new Store.AjaxAdapter();
@@ -384,12 +380,12 @@ export default class Store {
/**
* Unregister an event listener that was registered with on().
*
+ * @deprecated Use the store.observable property instead of this.
* @since 0.4.0
* @param {string} event - Name of the event.
* @param {string} type - Name of resource to originally passed to on().
* @param {string} [id] - ID of the resource to originally passed to on().
* @param {function} callback - Function originally passed to on().
- * @return {undefined} - Nothing.
*/
off(event, type, id, callback) {
if (event === "added" || event === "updated" || event === "removed") {
@@ -411,13 +407,13 @@ export default class Store {
/**
* Register an event listener: "added", "updated" or "removed".
*
+ * @deprecated Use the store.observable property instead of this.
* @since 0.4.0
* @param {string} event - Name of the event.
* @param {string} type - Name of resource to watch.
* @param {string} [id] - ID of the resource to watch.
* @param {function} callback - Function to call when the event occurs.
* @param {Object} [context] - Context in which to call the callback.
- * @return {undefined} - Nothing.
*/
on(event, type, id, callback, context) {
if (event === "added" || event === "updated" || event === "removed") {
@@ -455,7 +451,6 @@ export default class Store {
* @since 0.1.0
* @param {Object} root - Top Level Object to push. See:
http://jsonapi.org/format/#document-top-level
- * @return {undefined} - Nothing.
*/
push(root) {
if (root.data.constructor === Array) {
@@ -475,7 +470,6 @@ export default class Store {
* @param {!string} type - Type of the resource(s) to remove.
* @param {string} [id] - The id of the resource to remove. If omitted all
* resources of the type will be removed.
- * @return {undefined} - Nothing.
*/
remove(type, id) {
if (type) {
@@ -513,7 +507,6 @@ export default class Store {
* @param {function} [success] - Callback on success.
* @param {function} [error] - Callback on error.
* @param {Object} [context] - Context for the callbacks.
- * @return {undefined} - Nothing.
*
* @example
* let adapter = new Store.AjaxAdapter();
--
2.51.2