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);
+ });
+ });
+});