Skip to content

Commit

Permalink
issue #51, issue #45: added tests and improved implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gullerya committed Sep 1, 2020
1 parent af264f2 commit be78a5a
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 16 deletions.
39 changes: 28 additions & 11 deletions src/object-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const
REVERSE = 'reverse',
SHUFFLE = 'shuffle',
oMetaKey = Symbol('observable-meta-key'),
validObservableOptionKeys = { async: 1 },
validObserverOptionKeys = { path: 1, pathsOf: 1, pathsFrom: 1 },
processObserveOptions = function processObserveOptions(options) {
const result = {};
Expand Down Expand Up @@ -34,7 +35,7 @@ const
}
const invalidOptions = Object.keys(options).filter(option => !validObserverOptionKeys.hasOwnProperty(option));
if (invalidOptions.length) {
throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid option/s`);
throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid observer option/s`);
}
return result;
},
Expand Down Expand Up @@ -121,9 +122,10 @@ const
return result;
},
callObserversFromMT = function callObserversFromMT() {
const l = this.length;
for (let i = 0; i < l; i++) {
this[i][0](this[i][1]);
const batches = this.batches;
this.batches = null;
for (const b of batches) {
b[0](b[1]);
}
},
callObservers = function callObservers(oMeta, changes) {
Expand All @@ -143,16 +145,16 @@ const
if (relevantChanges.length) {
if (currentObservable.options.async) {
// this the async handling which
if (!currentObservable.options.batches) {
currentObservable.options.batches = [];
queueMicrotask(callObserversFromMT.bind(currentObservable.options.batches));
if (!currentObservable.batches) {
currentObservable.batches = [];
queueMicrotask(callObserversFromMT.bind(currentObservable));
}
let rb = currentObservable.options.batches.find(b => b[0] === target);
let rb = currentObservable.batches.find(b => b[0] === target);
if (!rb) {
rb = [target, []];
currentObservable.options.batches.push(rb);
currentObservable.batches.push(rb);
}
rb[1].push.apply(rb[1], relevantChanges);
Array.prototype.push.apply(rb[1], relevantChanges);
} else {
// this is the naive straight forward synchronous dispatch
target(relevantChanges);
Expand Down Expand Up @@ -538,7 +540,22 @@ class OMetaBase {
this.revokable = Proxy.revocable(targetClone, this);
this.proxy = this.revokable.proxy;
this.target = targetClone;
this.options = properties.options || {};
this.processOptions(properties.options);
}

processOptions(options) {
if (options) {
if (typeof options !== 'object') {
throw new Error(`Observable options if/when provided, MAY only be a non-null object, got '${options}'`);
}
const invalidOptions = Object.keys(options).filter(option => !validObservableOptionKeys.hasOwnProperty(option));
if (invalidOptions.length) {
throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid Observable option/s`);
}
this.options = Object.assign({}, options);
} else {
this.options = {};
}
}

detach() {
Expand Down
53 changes: 51 additions & 2 deletions tests/test-object-observer-objects-async.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,57 @@ import { Observable } from '../../dist/object-observer.js';

const suite = getSuite({ name: 'Testing Observable - async dispatch' });

suite.runTest({ name: 'multiple continuous mutations' }, async test => {
const
observable = Observable.from({}, { async: true }),
events = [];
let callbacks = 0;
observable.observe(changes => {
callbacks++;
events.push.apply(events, changes);
});

observable.a = 'some';
observable.b = 2;
observable.a = 'else';
delete observable.b;

await test.waitNextMicrotask();

test.assertEqual(1, callbacks);
test.assertEqual(4, events.length);
});

suite.runTest({ name: 'multiple continuous mutations is split bursts' }, async test => {
const
observable = Observable.from({}, { async: true }),
events = [];
let callbacks = 0;
observable.observe(changes => {
callbacks++;
events.push.apply(events, changes);
});

// first burst
observable.a = 1;
observable.b = 2;
await test.waitNextMicrotask();

test.assertEqual(1, callbacks);
test.assertEqual(2, events.length);

callbacks = 0;
events.splice(0);

// second burst
observable.a = 3;
observable.b = 4;
await test.waitNextMicrotask();

test.assertEqual(1, callbacks);
test.assertEqual(2, events.length);
});

suite.runTest({ name: 'Object.assign with multiple properties' }, async test => {
const
observable = Observable.from({}, { async: true }),
Expand All @@ -20,7 +71,6 @@ suite.runTest({ name: 'Object.assign with multiple properties' }, async test =>

test.assertEqual(1, callbacks);
test.assertEqual(3, events.length);
// TODO: add more assertions
});

suite.runTest({ name: 'Object.assign with multiple properties + more changes' }, async test => {
Expand All @@ -41,5 +91,4 @@ suite.runTest({ name: 'Object.assign with multiple properties + more changes' },

test.assertEqual(1, callbacks);
test.assertEqual(4, events.length);
// TODO: add more assertions
});
179 changes: 179 additions & 0 deletions tests/test-object-observer-performance-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { getSuite } from '../../node_modules/just-test/dist/just-test.js';
import { Observable } from '../../dist/object-observer.js';

const suite = getSuite({ name: 'Testing Observable Load - async' });

suite.runTest({ name: 'creating 100,000 observables, 1,000,000 deep (x3) mutations' }, async test => {
const
creationIterations = 100000,
mutationIterations = 1000000,
o = {
name: 'Anna Guller',
accountCreated: new Date(),
age: 20,
address: {
city: 'Dreamland',
street: {
name: 'Hope',
apt: 123
}
},
orders: []
};
let po,
changesCountA,
changesCountB,
started,
ended;

// creation of Observable
console.info('creating ' + creationIterations + ' observables from object...');
started = performance.now();
for (let i = 0; i < creationIterations; i++) {
po = Observable.from(o, { async: true });
}
ended = performance.now();
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');

// add listeners/callbacks
po.observe(changes => {
if (!changes.length) throw new Error('expected to have at least one change in the list');
else changesCountA += changes.length;
});
po.observe(changes => {
if (!changes) throw new Error('expected changes list to be defined');
else changesCountB += changes.length;
});

// mutation of existing property
changesCountA = 0;
changesCountB = 0;
console.info('performing ' + mutationIterations + ' deep (x3) primitive mutations...');
started = performance.now();
for (let i = 0; i < mutationIterations; i++) {
po.address.street.apt = i;
}
await test.waitNextMicrotask();
ended = performance.now();

if (changesCountA !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted A, but got ' + changesCountA);
if (changesCountB !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted B, but got ' + changesCountB);
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');

// adding new property
changesCountA = 0;
changesCountB = 0;
console.info('performing ' + mutationIterations + ' deep (x3) primitive additions...');
started = performance.now();
for (let i = 0; i < mutationIterations; i++) {
po.address.street[i] = i;
}
await test.waitNextMicrotask();
ended = performance.now();

if (changesCountA !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted A, but got ' + changesCountA);
if (changesCountB !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted B, but got ' + changesCountB);
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');

// removing new property
changesCountA = 0;
changesCountB = 0;
console.info('performing ' + mutationIterations + ' deep (x3) primitive deletions...');
started = performance.now();
for (let i = 0; i < mutationIterations; i++) {
delete po.address.street[i];
}
await test.waitNextMicrotask();
ended = performance.now();

if (changesCountA !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted A, but got ' + changesCountA);
if (changesCountB !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted B, but got ' + changesCountB);
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');
});

suite.runTest({ name: 'push 100,000 observables to an array, mutate them and pop them back' }, async test => {
const
mutationIterations = 100000,
o = {
name: 'Anna Guller',
accountCreated: new Date(),
age: 20,
address: {
city: 'Dreamland',
street: {
name: 'Hope',
apt: 123
}
},
orders: []
},
orders = [
{ id: 1, description: 'some description', sum: 1234, date: new Date() },
{ id: 2, description: 'some description', sum: 1234, date: new Date() },
{ id: 3, description: 'some description', sum: 1234, date: new Date() }
];
let changesCountA,
changesCountB,
started,
ended;

// creation of Observable
const po = Observable.from({ users: [] }, { async: true });

// add listeners/callbacks
po.observe(changes => {
if (!changes.length) throw new Error('expected to have at least one change in the list');
else changesCountA += changes.length;
});
po.observe(changes => {
if (!changes) throw new Error('expected changes list to be defined');
else changesCountB += changes.length;
});

// push objects
changesCountA = 0;
changesCountB = 0;
console.info('performing ' + mutationIterations + ' objects pushes...');
started = performance.now();
for (let i = 0; i < mutationIterations; i++) {
po.users.push(o);
}
await test.waitNextMicrotask();
ended = performance.now();

if (po.users.length !== mutationIterations) throw new Error('expected to have total of ' + mutationIterations + ' elements in pushed array, but got ' + po.length);
if (changesCountA !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted A, but got ' + changesCountA);
if (changesCountB !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted B, but got ' + changesCountB);
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');

// add orders array to each one of them
changesCountA = 0;
changesCountB = 0;
console.info('performing ' + mutationIterations + ' additions of arrays onto the objects...');
started = performance.now();
for (let i = 0; i < mutationIterations; i++) {
po.users[i].orders = orders;
}
await test.waitNextMicrotask();
ended = performance.now();

if (changesCountA !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted A, but got ' + changesCountA);
if (changesCountB !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted B, but got ' + changesCountB);
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');

// pop objects
changesCountA = 0;
changesCountB = 0;
console.info('performing ' + mutationIterations + ' object pops...');
started = performance.now();
for (let i = 0; i < mutationIterations; i++) {
po.users.pop();
}
await test.waitNextMicrotask();
ended = performance.now();

if (po.users.length !== 0) throw new Error('expected to have total of 0 elements in pushed array, but got ' + po.length);
if (changesCountA !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted A, but got ' + changesCountA);
if (changesCountB !== mutationIterations) throw new Error('expected to have ' + mutationIterations + ' changes counted B, but got ' + changesCountB);
console.info('\tdone: total time - ' + (ended - started) + 'ms, average operation time: ' + Math.round((ended - started) / mutationIterations * 10000) / 10000 + 'ms');
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getSuite } from '../../node_modules/just-test/dist/just-test.js';
import { Observable } from '../../dist/object-observer.js';

const suite = getSuite({ name: 'Testing Observable Load' });
const suite = getSuite({ name: 'Testing Observable Load - sync' });

suite.runTest({ name: 'creating 100,000 observables, 1,000,000 deep (x3) mutations' }, () => {
const
Expand Down
2 changes: 1 addition & 1 deletion tests/test-observe-specific-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ suite.runTest({

suite.runTest({
name: 'baseline - negative - no foreign options (pathFrom)',
expectError: 'is/are not a valid option/s'
expectError: 'is/are not a valid observer option/s'
}, () => {
const oo = Observable.from({ inner: { prop: 'more' } });
oo.observe(() => { }, { pathFrom: 'something' });
Expand Down
3 changes: 2 additions & 1 deletion tests/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
<script type="module" src="test-object-observer-objects.js"></script>
<script type="module" src="test-object-observer-objects-async.js"></script>
<script type="module" src="test-object-observer-objects-same-refs.js"></script>
<script type="module" src="test-object-observer-performance.js"></script>
<script type="module" src="test-object-observer-performance-async.js"></script>
<script type="module" src="test-object-observer-performance-sync.js"></script>
<script type="module" src="test-object-observer-subgraphs.js"></script>
<script type="module" src="test-observable-nested.js"></script>
<script type="module" src="test-observe-specific-paths.js"></script>
Expand Down

0 comments on commit be78a5a

Please sign in to comment.