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
+ * { name: string, type: string, id: string, resource: object }
+ *
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();