From 9515e5e278c7192bde4ee373b87335d6b254e776 Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Thu, 14 Nov 2019 16:12:39 +0100 Subject: [PATCH] Add observe class --- js/src/observe.js | 143 ++++++++++++++++++++++++++++++++++ js/test/observe.test.js | 169 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 js/src/observe.js create mode 100644 js/test/observe.test.js diff --git a/js/src/observe.js b/js/src/observe.js new file mode 100644 index 0000000..0125fd4 --- /dev/null +++ b/js/src/observe.js @@ -0,0 +1,143 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* eslint-disable no-underscore-dangle */ +export default class uObserve { + + /** + * Observe object's property or all properties if not specified. + * On change call observer function. + * observe(obj, prop, observer) observes given property prop; + * observe(obj, observer) observes all properties of object obj. + * @param {Object} obj + * @param {(string|function)} p1 + * @param {function=} p2 + */ + static observe(obj, p1, p2) { + if (typeof p2 === 'function') { + this.observeProperty(obj, p1, p2); + } else if (typeof p1 === 'function') { + if (Array.isArray(obj)) { + this.observeArray(obj, p1); + } else { + this.observeRecursive(obj, p1); + } + } else { + throw new Error('Invalid arguments'); + } + } + + /** + * Observe object's proporty. On change call observer + * @param {Object} obj + * @param {?string} property + * @param {function} observer + */ + static observeProperty(obj, property, observer) { + this.addObserver(obj, observer, property); + if (!obj.hasOwnProperty('_values')) { + Object.defineProperty(obj, '_values', { enumerable: false, configurable: false, value: [] }); + } + obj._values[property] = obj[property]; + Object.defineProperty(obj, property, { + get: () => obj._values[property], + set: (newValue) => { + if (obj._values[property] !== newValue) { + obj._values[property] = newValue; + uObserve.notify(obj._observers[property], newValue); + } + if (Array.isArray(obj[property])) { + this.observeArray(obj[property], observer); + } + } + }); + if (Array.isArray(obj[property])) { + this.observeArray(obj[property], observer); + } + } + + /** + * Recursively add observer to all properties + * @param {Object} obj + * @param {function} observer + */ + static observeRecursive(obj, observer) { + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + uObserve.observeProperty(obj, prop, observer); + } + } + } + + /** + * Observe array + * @param {Object} arr + * @param {function} observer + */ + static observeArray(arr, observer) { + this.addObserver(arr, observer); + [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach( + (operation) => { + const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, operation); + descriptor.value = function () { + const result = Array.prototype[operation].apply(arr, arguments); + uObserve.notify(arr._observers, arr); + return result; + }; + Object.defineProperty(arr, operation, descriptor); + }); + } + + /** + * Store observer in object + * @param {Object} obj Object + * @param {function} observer Observer + * @param {string=} property Optional property + */ + static addObserver(obj, observer, property) { + if (!obj.hasOwnProperty('_observers')) { + Object.defineProperty(obj, '_observers', { + enumerable: false, + configurable: false, + value: (arguments.length === 3) ? [] : new Set() + }); + } + if (arguments.length === 3) { + if (!obj._observers[property]) { + obj._observers[property] = new Set(); + } + obj._observers[property].add(observer); + } else { + obj._observers.add(observer); + } + } + + /** + * Notify observers + * @param {Set} observers + * @param {*} value + */ + static notify(observers, value) { + for (const observer of observers) { + (async () => { + await observer(value); + })(); + } + } +} diff --git a/js/test/observe.test.js b/js/test/observe.test.js new file mode 100644 index 0000000..247b24a --- /dev/null +++ b/js/test/observe.test.js @@ -0,0 +1,169 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import uObserve from '../src/observe.js'; + +describe('Observe tests', () => { + let object; + let result = false; + let resultValue; + + beforeEach(() => { + object = { observed: 1, nonObserved: 1 }; + result = false; + }); + + describe('when object is observed', () => { + + it('should throw error if observer is missing', () => { + expect(() => { uObserve.observe(object, 'observed'); }).toThrow(new Error('Invalid arguments')); + }); + + it('should notify observers when observed property is modified', () => { + // given + uObserve.observe(object, 'observed', (value) => { + result = true; + resultValue = value; + }); + // when + expect(result).toBe(false); + object.observed = 2; + // then + expect(result).toBe(true); + expect(resultValue).toBe(2); + }); + + it('should not notify observers when non-observed property is modified', () => { + // given + uObserve.observe(object, 'observed', () => { + result = true; + }); + // when + expect(result).toBe(false); + object.nonObserved = 2; + // then + expect(result).toBe(false); + }); + + it('should not notify observers when modified value is same', () => { + // given + uObserve.observe(object, 'observed', () => { + result = true; + }); + // when + expect(result).toBe(false); + object.observed = 1; + // then + expect(result).toBe(false); + }); + + it('should notify observers when any property is modified', () => { + // given + uObserve.observe(object, (value) => { + result = true; + resultValue = value; + }); + // when + expect(result).toBe(false); + object.observed = 2; + // then + expect(result).toBe(true); + expect(resultValue).toBe(2); + + // given + result = false; + resultValue = null; + + // when + expect(result).toBe(false); + object.nonObserved = 2; + // then + expect(result).toBe(true); + expect(resultValue).toBe(2); + }); + + it('should notify observers when observed array property is modified', () => { + // given + const array = [ 1, 2 ]; + object = { array: array }; + uObserve.observe(object, 'array', (value) => { + result = true; + resultValue = value; + }); + // when + expect(result).toBe(false); + array.push(3); + // then + expect(result).toBe(true); + expect(resultValue).toEqual(array); + }); + + it('should notify observers when observed array object is modified', () => { + // given + const array = [ 1, 2 ]; + uObserve.observe(array, (value) => { + result = true; + resultValue = value; + }); + // when + expect(result).toBe(false); + array.push(3); + // then + expect(result).toBe(true); + expect(resultValue).toEqual(array); + }); + + it('should retain observers after array is reassigned', () => { + // given + const array = [ 1, 2 ]; + const newArray = [ 3, 4 ]; + object = { array: array }; + uObserve.observe(object, 'array', (value) => { + result = true; + resultValue = value; + }); + // when + object.array = newArray; + result = false; + + expect(result).toBe(false); + object.array.push(5); + // then + expect(result).toBe(true); + expect(resultValue).toEqual(newArray); + }); + }); + + describe('when notify is called directly', () => { + it('should call observers with given value', () => { + // given + const observers = new Set(); + let result2 = false; + observers.add((value) => { result = value; }); + observers.add((value) => { result2 = value; }); + // when + expect(result).toBe(false); + expect(result2).toBe(false); + uObserve.notify(observers, true); + // then + expect(result).toBe(true); + expect(result2).toBe(true); + }); + }); +});