-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
296 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,10 @@ | ||
const VALUE = Symbol('value'), | ||
CALLBACKS = Symbol('callbacks'); | ||
LISTENERS = Symbol('listeners'), | ||
TEARDOWNS = Symbol('teardowns'), | ||
STATE = Symbol('state'), | ||
IDLE = 0, | ||
TEARING_DOWN = 1, | ||
CHANGING = 2; | ||
|
||
/** | ||
* An optional value | ||
|
@@ -14,7 +19,6 @@ const VALUE = Symbol('value'), | |
*/ | ||
const Optional = Fn.inherits('Develry.Placeholder', function Optional(value) { | ||
this[VALUE] = value; | ||
this[CALLBACKS] = []; | ||
}); | ||
|
||
/** | ||
|
@@ -46,14 +50,20 @@ Optional.setStatic(function unDry(value) { | |
Optional.setProperty(function value() { | ||
return this[VALUE]; | ||
}, function setValue(new_value) { | ||
this[VALUE] = new_value; | ||
|
||
let callbacks = this[CALLBACKS], | ||
i; | ||
return this.setValue(new_value); | ||
}); | ||
|
||
for (i = 0; i < callbacks.length; i++) { | ||
callbacks[i](new_value); | ||
} | ||
/** | ||
* Set a new value | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {*} new_value | ||
*/ | ||
Optional.setMethod(function setValue(new_value) { | ||
this[VALUE] = new_value; | ||
}); | ||
|
||
/** | ||
|
@@ -74,19 +84,6 @@ Optional.setMethod(function toDry() { | |
}; | ||
}); | ||
|
||
/** | ||
* Add a listener | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {Function} callback | ||
*/ | ||
Optional.setMethod(function onChange(callback) { | ||
this[CALLBACKS].push(callback); | ||
}); | ||
|
||
/** | ||
* Is there a value present? | ||
* | ||
|
@@ -131,4 +128,127 @@ Optional.setMethod(function orElse(fallback) { | |
*/ | ||
Optional.setMethod(function getResolvedValue() { | ||
return this[VALUE]; | ||
}); | ||
|
||
/** | ||
* The observable version of the optional value | ||
* | ||
* @constructor | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {*} value | ||
*/ | ||
const ObservableOptional = Fn.inherits('Develry.Optional', function ObservableOptional(value) { | ||
this[VALUE] = value; | ||
this[LISTENERS] = null; | ||
this[TEARDOWNS] = null; | ||
this[STATE] = IDLE; | ||
}); | ||
|
||
/** | ||
* Set a new value | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {*} new_value | ||
*/ | ||
ObservableOptional.setMethod(function setValue(new_value) { | ||
try { | ||
actuallySetValue(this, new_value); | ||
} finally { | ||
this[STATE] = IDLE; | ||
} | ||
}); | ||
|
||
/** | ||
* Actually set the value | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {*} new_value | ||
*/ | ||
const actuallySetValue = (target, new_value) => { | ||
|
||
// If the value did not change, return | ||
if (target[VALUE] === new_value) { | ||
return; | ||
} | ||
|
||
const old_value = target[VALUE]; | ||
target[VALUE] = new_value; | ||
|
||
// If we're already in the process of changing, return | ||
if (target[STATE] > IDLE) { | ||
return; | ||
} | ||
|
||
target[STATE] = TEARING_DOWN; | ||
|
||
while (target[TEARDOWNS]?.length) { | ||
target[TEARDOWNS].shift()(new_value, old_value); | ||
} | ||
|
||
target[STATE] = CHANGING; | ||
|
||
let listeners = target[LISTENERS]; | ||
|
||
if (listeners?.size) { | ||
|
||
let teardowns = [], | ||
teardown, | ||
listener; | ||
|
||
for (listener of listeners) { | ||
teardown = listener(new_value); | ||
|
||
if (teardown && typeof teardown == 'function') { | ||
teardowns.push(teardown); | ||
} | ||
} | ||
|
||
target[TEARDOWNS] = teardowns; | ||
} | ||
}; | ||
|
||
/** | ||
* Add a listener | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {Function} callback | ||
*/ | ||
ObservableOptional.setMethod(function addListener(callback) { | ||
|
||
if (!this[LISTENERS]) { | ||
this[LISTENERS] = new Set(); | ||
} | ||
|
||
this[LISTENERS].add(callback); | ||
}); | ||
|
||
/** | ||
* Remove a listener | ||
* | ||
* @author Jelle De Loecker <[email protected]> | ||
* @since 0.9.3 | ||
* @version 0.9.3 | ||
* | ||
* @param {Function} callback | ||
*/ | ||
ObservableOptional.setMethod(function removeListener(callback) { | ||
|
||
if (!this[LISTENERS]) { | ||
return; | ||
} | ||
|
||
this[LISTENERS].delete(callback); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
let assert = require('assert'), | ||
Blast, | ||
Optional, | ||
ObservableOptional; | ||
|
||
describe('Optional', () => { | ||
|
||
before(() => { | ||
// Get the Protoblast instance | ||
Blast = require('../index.js')(); | ||
Optional = Blast.Classes.Develry.Optional; | ||
ObservableOptional = Blast.Classes.Develry.ObservableOptional; | ||
}); | ||
|
||
describe('Optional', () => { | ||
describe('constructor', () => { | ||
it('should create an Optional instance with the given value', () => { | ||
const opt = new Optional(42); | ||
assert.strictEqual(opt.value, 42); | ||
}); | ||
}); | ||
|
||
describe('unDry', () => { | ||
it('should create an Optional instance from a dried value', () => { | ||
const dried = { value: 'hello' }; | ||
const opt = Optional.unDry(dried); | ||
assert.strictEqual(opt.value, 'hello'); | ||
assert.strictEqual(opt instanceof Optional, true); | ||
}); | ||
}); | ||
|
||
describe('setValue', () => { | ||
it('should set the value of the Optional instance', () => { | ||
const opt = new Optional(10); | ||
opt.setValue(20); | ||
assert.strictEqual(opt.value, 20); | ||
}); | ||
}); | ||
|
||
describe('toDry', () => { | ||
it('should be used to dry the instance', () => { | ||
const opt = new Optional('foo'); | ||
let dried = Blast.Classes.JSON.dry(opt); | ||
let undried = Blast.Classes.JSON.undry(dried); | ||
|
||
assert.strictEqual(undried.value, 'foo'); | ||
assert.strictEqual(undried instanceof Optional, true); | ||
}); | ||
}); | ||
|
||
describe('isPresent', () => { | ||
it('should return true if the Optional instance has a value', () => { | ||
const opt = new Optional(true); | ||
assert.strictEqual(opt.isPresent(), true); | ||
}); | ||
|
||
it('should return false if the Optional instance has no value', () => { | ||
const opt = new Optional(null); | ||
assert.strictEqual(opt.isPresent(), false); | ||
}); | ||
}); | ||
|
||
describe('orElse', () => { | ||
it('should return the value if present', () => { | ||
const opt = new Optional(42); | ||
assert.strictEqual(opt.orElse(10), 42); | ||
}); | ||
|
||
it('should return the fallback value if not present', () => { | ||
const opt = new Optional(null); | ||
assert.strictEqual(opt.orElse(10), 10); | ||
}); | ||
}); | ||
|
||
describe('getResolvedValue', () => { | ||
it('should return the value of the Optional instance', () => { | ||
const opt = new Optional('bar'); | ||
assert.strictEqual(opt.getResolvedValue(), 'bar'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('ObservableOptional', () => { | ||
describe('constructor', () => { | ||
it('should create an ObservableOptional instance with the given value', () => { | ||
const opt = new ObservableOptional(42); | ||
assert.strictEqual(opt.value, 42); | ||
assert.strictEqual(opt instanceof ObservableOptional, true); | ||
assert.strictEqual(opt instanceof Optional, true); | ||
}); | ||
}); | ||
|
||
describe('setValue', () => { | ||
it('should set the value and notify listeners', () => { | ||
const opt = new ObservableOptional(10); | ||
let newValue; | ||
|
||
opt.addListener((value) => { | ||
newValue = value; | ||
}); | ||
|
||
opt.setValue(20); | ||
assert.strictEqual(opt.value, 20); | ||
assert.strictEqual(newValue, 20); | ||
}); | ||
|
||
it('should not notify listeners if the value did not change', () => { | ||
const opt = new ObservableOptional(10); | ||
let notifiedCount = 0; | ||
|
||
opt.addListener(() => { | ||
notifiedCount++; | ||
}); | ||
|
||
opt.setValue(10); | ||
assert.strictEqual(notifiedCount, 0); | ||
}); | ||
}); | ||
|
||
describe('addListener', () => { | ||
it('should add a listener that is called when the value changes', () => { | ||
const opt = new ObservableOptional(10); | ||
let newValue; | ||
|
||
opt.addListener((value) => { | ||
newValue = value; | ||
}); | ||
|
||
opt.setValue(20); | ||
assert.strictEqual(newValue, 20); | ||
}); | ||
}); | ||
|
||
describe('removeListener', () => { | ||
it('should remove a previously added listener', () => { | ||
const opt = new ObservableOptional(10); | ||
let notifiedCount = 0; | ||
|
||
const listener = (value) => { | ||
notifiedCount++; | ||
}; | ||
|
||
opt.addListener(listener); | ||
opt.setValue(20); | ||
assert.strictEqual(notifiedCount, 1); | ||
|
||
opt.removeListener(listener); | ||
opt.setValue(30); | ||
assert.strictEqual(notifiedCount, 1); | ||
}); | ||
}); | ||
}); |