Skip to content
This repository has been archived by the owner on Jan 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #14 from Asana/edge-cases
Browse files Browse the repository at this point in the history
Edge cases
  • Loading branch information
pspeter3 committed Jan 22, 2015
2 parents 1177347 + d8dcb23 commit 7bbcc2f
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 89 deletions.
116 changes: 47 additions & 69 deletions src/perishable.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,58 @@
import Handle = require("./handle");

interface PerishableNodePointer<T> {
next(): PerishableNode<T>;
setNext(node: PerishableNode<T>): void;
interface PerishableNodeIterator<T> {
callback: () => any;
next: PerishableNode<T>;
}

class PerishableSentinel<T> implements PerishableNodePointer<T> {
private _next: PerishableNode<T>;
private _onUnused: () => any;

constructor(onUnused: () => any) {
this._next = null;
this._onUnused = onUnused;
}

isUnused(): boolean {
return this._next === null;
}

next(): PerishableNode<T> {
return this._next;
}

setNext(node: PerishableNode<T>): void {
this._next = node;
if (this._next === null && this._onUnused) {
this._onUnused();
}
}
}

class PerishableNode<T> implements Handle<T>, PerishableNodePointer<T> {
private _value: T;
private _onStale: () => any;
_prev: PerishableNodePointer<T>;
class PerishableNode<T> implements Handle<T> {
_value: T;
_callback: () => any;
_prev: PerishableNode<T>;
_next: PerishableNode<T>;

constructor(value: T, onStale: () => any, prev: PerishableNodePointer<T>) {
constructor(value: T, callback: () => any, prev: PerishableNode<T> = null) {
this._value = value;
this._onStale = onStale;
this._callback = callback;
this._prev = prev;
this._next = this._prev.next();
this._prev.setNext(this);
if (this.hasNext()) {
this._next._prev = this;
this._next = null;
if (this._hasPrev()) {
this._setNext(this._prev._next);
this._prev._setNext(this);
}
if (this._hasNext()) {
this._next._setPrev(this);
}
}

value(): T {
return this._value;
_setPrev(prev: PerishableNode<T>): void {
this._prev = prev;
}

release(): void {
this._prev.setNext(this._next);
if (this.hasNext()) {
this._next._prev = this._prev;
_setNext(next: PerishableNode<T>): void {
this._next = next;
// We only automatically call the callback when the head is unused
if (!this._hasPrev() && !this._hasNext()) {
this._callback();
}
}

next(): PerishableNode<T> {
return this._next;
_hasPrev(): boolean {
return this._prev !== null;
}

setNext(node: PerishableNode<T>): void {
this._next = node;
_hasNext(): boolean {
return this._next !== null;
}

hasNext(): boolean {
return this._next !== null;
value(): T {
return this._value;
}

onStale(): void {
if (this._onStale) {
this._onStale();
release(): void {
this._prev._setNext(this._next);
if (this._hasNext()) {
this._next._setPrev(this._prev);
}
}
}
Expand All @@ -82,9 +62,7 @@ class PerishableNode<T> implements Handle<T>, PerishableNodePointer<T> {
*/
class Perishable<T> {
private _value: T;
private _onUnused: () => any;
private _isStale: boolean;
private _sentinel: PerishableSentinel<T>;
private _head: PerishableNode<T>;

/**
* Create a new perishable
Expand All @@ -93,9 +71,7 @@ class Perishable<T> {
*/
constructor(value: T, onUnused: () => any) {
this._value = value;
this._onUnused = onUnused;
this._isStale = false;
this._sentinel = new PerishableSentinel<T>(this._onUnused);
this._head = new PerishableNode<T>(value, onUnused);
}

/**
Expand All @@ -111,15 +87,15 @@ class Perishable<T> {
* @returns {boolean}
*/
isStale(): boolean {
return this._isStale;
return this._head === null;
}

/**
* Whether or not the perishable has any handles
* @returns {boolean}
*/
isUnused(): boolean {
return this._sentinel.isUnused();
return this.isStale() || !this._head._hasNext();
}

/**
Expand All @@ -131,22 +107,24 @@ class Perishable<T> {
if (this.isStale()) {
throw new Error("Cannot createHandle when stale");
}
return new PerishableNode<T>(this._value, onStale, this._sentinel);
return new PerishableNode<T>(this._value, onStale, this._head);
}

/**
* Make the reference stale and notify all handles
*/
makeStale(): void {
if (!this.isStale()) {
var iterator: PerishableNode<T>;
iterator = (<PerishableNode<T>>this._sentinel.next());
while (iterator !== null) {
iterator.onStale();
iterator = (<PerishableNode<T>>iterator.next());
var node = this._head;
this._head = null;
var callbacks: Function[] = [];
while (node !== null) {
callbacks.push(node._callback);
node = node._next;
}
for (var i = callbacks.length - 1; i >= 0; i--) {
callbacks[i]();
}
this._isStale = true;
this._sentinel = null;
}
}
}
Expand Down
107 changes: 87 additions & 20 deletions test/perishable_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("Perishable", () => {

describe("constructor", () => {
it("should create a perishable", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
assert.instanceOf(perishable, tsutil.Perishable);
});

Expand All @@ -19,40 +19,48 @@ describe("Perishable", () => {
perishable.createHandle(null).release();
sinon.assert.calledOnce(spy);
});

it("should call onUnused every time", () => {
var spy = sinon.spy();
var perishable = new tsutil.Perishable(VALUE, spy);
perishable.createHandle(null).release();
perishable.createHandle(null).release();
sinon.assert.calledTwice(spy);
});
});

describe("value", () => {
it("should return the value", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
assert.equal(perishable.value(), VALUE);
});
});

describe("isStale", () => {
it("should return false by default", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
assert.isFalse(perishable.isStale());
});
});

describe("isUnused", () => {
it("should be unused by default", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
assert.isTrue(perishable.isUnused());
});
});

describe("createHandle", () => {
it("should create a handle", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var spy = sinon.spy();
var handle = perishable.createHandle(spy);
assert.equal(handle.value(), VALUE);
sinon.assert.notCalled(spy);
});

it("should throw if stale", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
perishable.makeStale();
assert.throws(() => {
perishable.createHandle(null);
Expand All @@ -62,21 +70,21 @@ describe("Perishable", () => {

describe("makeStale", () => {
it("should make the projection stale", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
perishable.makeStale();
assert.isTrue(perishable.isStale());
});

it("should notify a handle", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var spy = sinon.spy();
perishable.createHandle(spy);
perishable.makeStale();
sinon.assert.calledOnce(spy);
});

it("should notify multiple handles", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var first = sinon.spy();
var second = sinon.spy();
perishable.createHandle(first);
Expand All @@ -86,14 +94,8 @@ describe("Perishable", () => {
sinon.assert.calledOnce(second);
});

it("should handle a null handle", () => {
var perishable = new tsutil.Perishable(VALUE, null);
perishable.createHandle(null);
perishable.makeStale();
});

it("should not notify a handle when stale", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var spy = sinon.spy();
perishable.createHandle(spy);
perishable.makeStale();
Expand All @@ -102,7 +104,7 @@ describe("Perishable", () => {
});

it("should not notify a released handle", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var spy = sinon.spy();
var handle = perishable.createHandle(spy);
handle.release();
Expand All @@ -111,7 +113,7 @@ describe("Perishable", () => {
});

it("should not notify a released handle at the beginning of the list", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var first = sinon.spy();
var second = sinon.spy();
perishable.createHandle(first).release();
Expand All @@ -122,7 +124,7 @@ describe("Perishable", () => {
});

it("should not notify a released handle at the end of the list", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var first = sinon.spy();
var second = sinon.spy();
perishable.createHandle(first);
Expand All @@ -133,7 +135,7 @@ describe("Perishable", () => {
});

it("should not notify a released handle in a list", () => {
var perishable = new tsutil.Perishable(VALUE, null);
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var first = sinon.spy();
var second = sinon.spy();
var third = sinon.spy();
Expand All @@ -146,5 +148,70 @@ describe("Perishable", () => {
sinon.assert.notCalled(second);
sinon.assert.calledOnce(third);
});

it("should handle isUnused after correctly", () => {
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
perishable.createHandle(sinon.spy());
perishable.createHandle(sinon.spy());
perishable.makeStale();
assert.isTrue(perishable.isUnused());
});

it("should handle first release after correctly", () => {
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
var first = perishable.createHandle(sinon.spy());
perishable.createHandle(sinon.spy());
perishable.makeStale();
first.release();
});

it("should handle second release after correctly", () => {
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
perishable.createHandle(sinon.spy());
var second = perishable.createHandle(sinon.spy());
perishable.makeStale();
second.release();
});

it("should be stale once make stale is called", () => {
var perishable = new tsutil.Perishable(VALUE, sinon.spy());
perishable.createHandle(() => { assert.isTrue(perishable.isStale()); });
perishable.createHandle(() => { assert.isTrue(perishable.isStale()); });
perishable.makeStale();
});

it("should call onUnused when becoming stale", () => {
var spy = sinon.spy();
var perishable = new tsutil.Perishable(VALUE, spy);
perishable.makeStale();
sinon.assert.calledOnce(spy);
});

it("should handle a release call during makeStale", () => {
var onUnused = sinon.spy();
var spy = sinon.spy();
var perishable = new tsutil.Perishable(VALUE, onUnused);
var toRelease = perishable.createHandle(spy);
perishable.createHandle(() => {
toRelease.release();
});
perishable.makeStale();
sinon.assert.calledOnce(onUnused);
sinon.assert.calledOnce(spy);
});

it("should handle another release call during makeStale", () => {
var onUnused = sinon.spy();
var spy = sinon.spy();
var perishable = new tsutil.Perishable(VALUE, onUnused);
var toRelease: tsutil.Releasable;
perishable.createHandle(() => {
toRelease.release();
});
toRelease = perishable.createHandle(spy);
perishable.makeStale();
sinon.assert.calledOnce(onUnused);
sinon.assert.calledOnce(spy);
});
});
});

0 comments on commit 7bbcc2f

Please sign in to comment.