From 9e401fd07488e73e6bdbc3e7cf9abc75f2f65ae4 Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Fri, 29 Dec 2023 12:15:05 +0200 Subject: [PATCH 1/4] add timeout support to blocking queue operations --- dist/index.d.ts | 5 +++-- dist/index.js | 41 ++++++++++++++++++++++----------------- dist/libs/LinkedList.js | 2 +- src/index.ts | 43 +++++++++++++++++++++++++++-------------- tests/WaitQueue.test.ts | 12 ++++++++++++ 5 files changed, 67 insertions(+), 36 deletions(-) diff --git a/dist/index.d.ts b/dist/index.d.ts index 92537ba..44b78cd 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -14,8 +14,9 @@ declare class WaitQueue { clearListeners(): void; unshift(...items: T[]): number; push(...items: T[]): number; - shift(): Promise; - pop(): Promise; + private _remove; + shift(timeout?: number): Promise; + pop(timeout?: number): Promise; private _flush; } export = WaitQueue; diff --git a/dist/index.js b/dist/index.js index 4ef58ac..3656410 100644 --- a/dist/index.js +++ b/dist/index.js @@ -54,7 +54,7 @@ var WaitQueue = /** @class */ (function () { get: function () { return this.queue.length; }, - enumerable: true, + enumerable: false, configurable: true }); WaitQueue.prototype.empty = function () { @@ -100,38 +100,43 @@ var WaitQueue = /** @class */ (function () { this._flush(); return this.length; }; - WaitQueue.prototype.shift = function () { + WaitQueue.prototype._remove = function (type, timeout) { var _this = this; return new Promise(function (resolve, reject) { if (_this.queue.length > 0) { return resolve(_this.queue.shift()); } else { + var timedOut_1 = false; + if (timeout && timeout > 0) { + setTimeout(function () { + timedOut_1 = true; + reject("pop timed out"); + }, timeout); + } _this.listeners.push(function (err) { if (err) { return reject(err); } - return resolve(_this.queue.shift()); - }); - } - }); - }; - WaitQueue.prototype.pop = function () { - var _this = this; - return new Promise(function (resolve, reject) { - if (_this.queue.length > 0) { - return resolve(_this.queue.pop()); - } - else { - _this.listeners.push(function (err) { - if (err) { - return reject(err); + if (timedOut_1) { + return _this._flush(); + } + switch (type) { + case 'SHIFT': + return resolve(_this.queue.shift()); + case 'POP': + return resolve(_this.queue.pop()); } - return resolve(_this.queue.pop()); }); } }); }; + WaitQueue.prototype.shift = function (timeout) { + return this._remove('SHIFT', timeout); + }; + WaitQueue.prototype.pop = function (timeout) { + return this._remove('POP', timeout); + }; WaitQueue.prototype._flush = function () { if (this.queue.length > 0 && this.listeners.length > 0) { var listener = this.listeners.shift(); diff --git a/dist/libs/LinkedList.js b/dist/libs/LinkedList.js index 88eec0d..6432d48 100644 --- a/dist/libs/LinkedList.js +++ b/dist/libs/LinkedList.js @@ -17,7 +17,7 @@ var LinkedList = /** @class */ (function () { get: function () { return this._length; }, - enumerable: true, + enumerable: false, configurable: true }); LinkedList.prototype.empty = function () { diff --git a/src/index.ts b/src/index.ts index 90c46e1..2bcbaed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,35 +43,48 @@ class WaitQueue { this._flush(); return this.length; } - shift(): Promise { + + private _remove(type: "SHIFT" | "POP", timeout?: number): Promise { return new Promise((resolve, reject) => { if (this.queue.length > 0) { return resolve(this.queue.shift()); } else { + let timedOut = false; + + if (timeout && timeout > 0) { + setTimeout(() => { + timedOut = true; + reject("pop timed out"); + }, timeout); + } + this.listeners.push((err: Error) => { if (err) { return reject(err); } - return resolve(this.queue.shift()); - }); - } - }); - } - pop(): Promise { - return new Promise((resolve, reject) => { - if (this.queue.length > 0) { - return resolve(this.queue.pop()); - } else { - this.listeners.push((err: Error) => { - if (err) { - return reject(err); + + if (timedOut) { + return this._flush(); + } + + switch (type) { + case 'SHIFT': + return resolve(this.queue.shift()); + case 'POP': + return resolve(this.queue.pop()); } - return resolve(this.queue.pop()); }); } }); } + shift(timeout?: number): Promise { + return this._remove('SHIFT', timeout); + } + pop(timeout?: number): Promise { + return this._remove('POP', timeout); + } + private _flush(): void { if (this.queue.length > 0 && this.listeners.length > 0) { const listener = this.listeners.shift(); diff --git a/tests/WaitQueue.test.ts b/tests/WaitQueue.test.ts index f450f0e..8b4d725 100644 --- a/tests/WaitQueue.test.ts +++ b/tests/WaitQueue.test.ts @@ -78,6 +78,18 @@ describe('Methods of WaitQueue', function() { assert.ok(wq.pop() instanceof Promise); }); + it('pop(timeout) should error out', async function() { + assert.rejects(wq.pop(1)) + }); + + it('timed out pop should pop next one', async function() { + const p1 = wq.pop(1); + const p2 = wq.pop(); + setTimeout(() => wq.push(1), 100); + assert.rejects(p1); + assert.strictEqual(await p2, 1); + }) + it('shift() should wait while empty', async function() { const obj = { name: 'test' }; setTimeout(() => { From e93bc36bd15e30973cc639d66840256352f24f4b Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Fri, 29 Dec 2023 12:33:09 +0200 Subject: [PATCH 2/4] fix not doing shift/pop correctly in base case. points to lack in tests that only test with one element in queue so pop/shift return same result --- dist/index.js | 9 +++++++-- src/index.ts | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/dist/index.js b/dist/index.js index 3656410..d9c1f95 100644 --- a/dist/index.js +++ b/dist/index.js @@ -104,14 +104,19 @@ var WaitQueue = /** @class */ (function () { var _this = this; return new Promise(function (resolve, reject) { if (_this.queue.length > 0) { - return resolve(_this.queue.shift()); + switch (type) { + case 'SHIFT': + return resolve(_this.queue.shift()); + case 'POP': + return resolve(_this.queue.pop()); + } } else { var timedOut_1 = false; if (timeout && timeout > 0) { setTimeout(function () { timedOut_1 = true; - reject("pop timed out"); + reject("timed out"); }, timeout); } _this.listeners.push(function (err) { diff --git a/src/index.ts b/src/index.ts index 2bcbaed..e247a81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,14 +47,19 @@ class WaitQueue { private _remove(type: "SHIFT" | "POP", timeout?: number): Promise { return new Promise((resolve, reject) => { if (this.queue.length > 0) { - return resolve(this.queue.shift()); + switch (type) { + case 'SHIFT': + return resolve(this.queue.shift()); + case 'POP': + return resolve(this.queue.pop()); + } } else { let timedOut = false; if (timeout && timeout > 0) { setTimeout(() => { timedOut = true; - reject("pop timed out"); + reject("timed out"); }, timeout); } @@ -69,7 +74,7 @@ class WaitQueue { switch (type) { case 'SHIFT': - return resolve(this.queue.shift()); + return resolve(this.queue.shift()); case 'POP': return resolve(this.queue.pop()); } From 55066f8ddddc11c5845655cf0e972485bc770308 Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Sat, 30 Dec 2023 18:26:38 +0200 Subject: [PATCH 3/4] more changes * small refactor of LinkedList code for symmetry * fix time out handling, so clearListeners doesn't error out tiemd out handler * remove only handling one pending listener per event loop loop, handle all * count the number of listeners - and due to how they remain on the listener list until processed, keep a delta for the timed out ones. * cleanup the duplicative switches for pop/shift with some .bind() magic * improve/add to tests --- dist/index.d.ts | 2 ++ dist/index.js | 59 ++++++++++++++++++----------------- dist/libs/LinkedList.d.ts | 2 +- dist/libs/LinkedList.js | 42 ++++++++++++------------- src/index.ts | 65 +++++++++++++++++++++------------------ src/libs/LinkedList.ts | 41 ++++++++++++------------ tests/WaitQueue.test.ts | 58 ++++++++++++++++++++++++++-------- 7 files changed, 157 insertions(+), 112 deletions(-) diff --git a/dist/index.d.ts b/dist/index.d.ts index 44b78cd..dbf94f4 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -9,6 +9,8 @@ declare class WaitQueue { queue: LinkedList; listeners: LinkedList; get length(): number; + numListeners(): number; + numListenersDelta: number; empty(): void; clear(): void; clearListeners(): void; diff --git a/dist/index.js b/dist/index.js index d9c1f95..471d38d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -38,17 +38,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { * https://github.com/flarestart/wait-queue */ var LinkedList_1 = __importDefault(require("./libs/LinkedList")); -var nextLoop = (function () { - if (typeof setImmediate === 'function') { - return setImmediate; - } - /* istanbul ignore next */ - return function (fn) { return setTimeout(fn, 0); }; -})(); var WaitQueue = /** @class */ (function () { function WaitQueue() { this.queue = new LinkedList_1.default(); this.listeners = new LinkedList_1.default(); + this.numListenersDelta = 0; } Object.defineProperty(WaitQueue.prototype, "length", { get: function () { @@ -57,6 +51,9 @@ var WaitQueue = /** @class */ (function () { enumerable: false, configurable: true }); + WaitQueue.prototype.numListeners = function () { + return this.listeners.length - this.numListenersDelta; + }; WaitQueue.prototype.empty = function () { this.queue = new LinkedList_1.default(); }; @@ -79,6 +76,7 @@ var WaitQueue = /** @class */ (function () { finally { if (e_1) throw e_1.error; } } this.listeners = new LinkedList_1.default(); + this.numListenersDelta = 0; }; WaitQueue.prototype.unshift = function () { var _a; @@ -102,36 +100,43 @@ var WaitQueue = /** @class */ (function () { }; WaitQueue.prototype._remove = function (type, timeout) { var _this = this; + var fn; + switch (type) { + case 'SHIFT': + fn = this.queue.shift.bind(this.queue); + break; + case 'POP': + fn = this.queue.pop.bind(this.queue); + break; + } return new Promise(function (resolve, reject) { + var self = _this; if (_this.queue.length > 0) { - switch (type) { - case 'SHIFT': - return resolve(_this.queue.shift()); - case 'POP': - return resolve(_this.queue.pop()); - } + return resolve(fn()); } else { var timedOut_1 = false; - if (timeout && timeout > 0) { + var timerId_1 = (timeout && timeout > 0) ? setTimeout(function () { + self.numListenersDelta++; timedOut_1 = true; - reject("timed out"); - }, timeout); - } + timerId_1 = undefined; + reject(new Error("Timed Out")); + }, timeout) : undefined; _this.listeners.push(function (err) { - if (err) { - return reject(err); + if (timerId_1) { + clearTimeout(timerId_1); + timerId_1 = undefined; } if (timedOut_1) { - return _this._flush(); + self.numListenersDelta--; + // already rejected, doesn't matter if err via clearListeners + return; } - switch (type) { - case 'SHIFT': - return resolve(_this.queue.shift()); - case 'POP': - return resolve(_this.queue.pop()); + if (err) { + return reject(err); } + return resolve(fn()); }); } }); @@ -143,11 +148,9 @@ var WaitQueue = /** @class */ (function () { return this._remove('POP', timeout); }; WaitQueue.prototype._flush = function () { - if (this.queue.length > 0 && this.listeners.length > 0) { + while (this.queue.length > 0 && this.listeners.length > 0) { var listener = this.listeners.shift(); listener.call(this); - // delay next loop - nextLoop(this._flush.bind(this)); } }; return WaitQueue; diff --git a/dist/libs/LinkedList.d.ts b/dist/libs/LinkedList.d.ts index 5773c6d..d30bb65 100644 --- a/dist/libs/LinkedList.d.ts +++ b/dist/libs/LinkedList.d.ts @@ -16,8 +16,8 @@ declare class LinkedList { get length(): number; empty(): void; push(...items: any[]): number; - shift(): any; unshift(...items: any[]): number; pop(): any; + shift(): any; } export default LinkedList; diff --git a/dist/libs/LinkedList.js b/dist/libs/LinkedList.js index 6432d48..5e1e4f0 100644 --- a/dist/libs/LinkedList.js +++ b/dist/libs/LinkedList.js @@ -33,36 +33,19 @@ var LinkedList = /** @class */ (function () { } items.forEach(function (item) { var node = createNode(item); - if (_this._front && _this._end) { - _this._end._next = node; - node._prev = _this._end; + if (_this._end === null) { + _this._front = node; _this._end = node; } else { - _this._front = node; + _this._end._next = node; + node._prev = _this._end; _this._end = node; } _this._length++; }); return this._length; }; - LinkedList.prototype.shift = function () { - var item = this._front; - if (item === null) { - return null; - } - if (item._next != null) { - this._front = item._next; - this._front._prev = null; - } - else { - this._front = null; - this._end = null; - } - item._next = null; - this._length--; - return item.item; - }; LinkedList.prototype.unshift = function () { var _this = this; var items = []; @@ -101,6 +84,23 @@ var LinkedList = /** @class */ (function () { item._prev = null; return item.item; }; + LinkedList.prototype.shift = function () { + var item = this._front; + if (item === null) { + return null; + } + if (item._next != null) { + this._front = item._next; + this._front._prev = null; + } + else { + this._front = null; + this._end = null; + } + item._next = null; + this._length--; + return item.item; + }; return LinkedList; }()); /* istanbul ignore next */ diff --git a/src/index.ts b/src/index.ts index e247a81..584b598 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,14 +4,6 @@ */ import LinkedList from './libs/LinkedList'; -const nextLoop = (() => { - if (typeof setImmediate === 'function') { - return setImmediate; - } - /* istanbul ignore next */ - return (fn: (...args: any[]) => void) => setTimeout(fn, 0); -})(); - class WaitQueue { [Symbol.iterator]: () => { next: () => { value: any; done: boolean } }; @@ -21,6 +13,12 @@ class WaitQueue { get length(): number { return this.queue.length; } + + numListeners(): number { + return this.listeners.length - this.numListenersDelta; + } + numListenersDelta = 0; + empty(): void { this.queue = new LinkedList(); } @@ -32,6 +30,7 @@ class WaitQueue { listener(new Error('Clear Listeners')); } this.listeners = new LinkedList(); + this.numListenersDelta = 0; } unshift(...items: T[]): number { this.queue.unshift(...items); @@ -45,39 +44,47 @@ class WaitQueue { } private _remove(type: "SHIFT" | "POP", timeout?: number): Promise { + let fn: () => any; + switch (type) { + case 'SHIFT': + fn = this.queue.shift.bind(this.queue); + break; + case 'POP': + fn = this.queue.pop.bind(this.queue); + break; + } + return new Promise((resolve, reject) => { + const self = this; if (this.queue.length > 0) { - switch (type) { - case 'SHIFT': - return resolve(this.queue.shift()); - case 'POP': - return resolve(this.queue.pop()); - } + return resolve(fn()); } else { let timedOut = false; - - if (timeout && timeout > 0) { + let timerId = (timeout && timeout > 0) ? setTimeout(() => { + self.numListenersDelta++; timedOut = true; - reject("timed out"); - }, timeout); - } + timerId = undefined; + reject(new Error("Timed Out")); + }, timeout) : undefined; this.listeners.push((err: Error) => { - if (err) { - return reject(err); + if (timerId) { + clearTimeout(timerId); + timerId = undefined; } if (timedOut) { - return this._flush(); + self.numListenersDelta--; + // already rejected, doesn't matter if err via clearListeners + return; } - switch (type) { - case 'SHIFT': - return resolve(this.queue.shift()); - case 'POP': - return resolve(this.queue.pop()); + if (err) { + return reject(err); } + + return resolve(fn()); }); } }); @@ -91,11 +98,9 @@ class WaitQueue { } private _flush(): void { - if (this.queue.length > 0 && this.listeners.length > 0) { + while (this.queue.length > 0 && this.listeners.length > 0) { const listener = this.listeners.shift(); listener.call(this); - // delay next loop - nextLoop(this._flush.bind(this)); } } } diff --git a/src/libs/LinkedList.ts b/src/libs/LinkedList.ts index 1921f90..395dd9e 100644 --- a/src/libs/LinkedList.ts +++ b/src/libs/LinkedList.ts @@ -34,34 +34,18 @@ class LinkedList { push(...items: any[]) { items.forEach(item => { const node = createNode(item); - if (this._front && this._end) { - this._end._next = node; - node._prev = this._end; + if (this._end === null) { + this._front = node; this._end = node; } else { - this._front = node; + this._end._next = node; + node._prev = this._end; this._end = node; } this._length++; }); return this._length; } - shift() { - const item = this._front; - if (item === null) { - return null; - } - if (item._next != null) { - this._front = item._next; - this._front._prev = null; - } else { - this._front = null; - this._end = null; - } - item._next = null; - this._length--; - return item.item; - } unshift(...items: any[]) { items.forEach(item => { const node = createNode(item); @@ -93,6 +77,23 @@ class LinkedList { item._prev = null; return item.item; } + shift() { + const item = this._front; + if (item === null) { + return null; + } + if (item._next != null) { + this._front = item._next; + this._front._prev = null; + } else { + this._front = null; + this._end = null; + } + item._next = null; + this._length--; + return item.item; + } + } /* istanbul ignore next */ diff --git a/tests/WaitQueue.test.ts b/tests/WaitQueue.test.ts index 8b4d725..40acc56 100644 --- a/tests/WaitQueue.test.ts +++ b/tests/WaitQueue.test.ts @@ -78,18 +78,6 @@ describe('Methods of WaitQueue', function() { assert.ok(wq.pop() instanceof Promise); }); - it('pop(timeout) should error out', async function() { - assert.rejects(wq.pop(1)) - }); - - it('timed out pop should pop next one', async function() { - const p1 = wq.pop(1); - const p2 = wq.pop(); - setTimeout(() => wq.push(1), 100); - assert.rejects(p1); - assert.strictEqual(await p2, 1); - }) - it('shift() should wait while empty', async function() { const obj = { name: 'test' }; setTimeout(() => { @@ -112,6 +100,52 @@ describe('Methods of WaitQueue', function() { assert.equal(obj.name, 'test'); }); + it('multiple shifts are in correct order', async function() { + wq.push(0, 1, 2, 3, 4); + for(let i=0; i < 5; i++) { + assert.strictEqual(await wq.shift(), i) + } + }) + + it('multiple pops are in correct order', async function() { + wq.push(0, 1, 2, 3, 4); + for(let i=4; i >= 0; i--) { + assert.strictEqual(await wq.pop(), i) + } + }) + + it('pop(timeout) should error out', async function() { + assert.rejects(wq.pop(1)) + }); + + it('timed out listener, should try next listener', async function() { + const p1 = wq.pop(1); + const p2 = wq.pop(); + setTimeout(() => wq.push(1), 10); + try { + await p1; + assert.strictEqual(true, false); + } catch (err) { + assert.deepStrictEqual(err, new Error("Timed Out")) + } + assert.strictEqual(await p2, 1); + }) + + it('count number of listeners', async function() { + const p1 = wq.pop(1); + const p2 = wq.pop(); + wq.pop().catch(() => {}); // this will be cleared, and hence failed; + + assert.strictEqual(wq.numListeners(), 3); + setTimeout(() => assert.strictEqual(wq.numListeners(), 2), 10); + setTimeout(() => wq.push(1), 20); + assert.rejects(p1); + assert.strictEqual(await p2, 1); + assert.strictEqual(wq.numListeners(), 1) + wq.clearListeners(); + assert.strictEqual(wq.numListeners(), 0); + }) + it('Iterator for(... of ...)', function() { for (let n = 0; n < 5; n++) { wq.push(n); From 5e1620a3a7bbfcb2c4747ea50a818d543c75b248 Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Sat, 30 Dec 2023 22:43:46 +0200 Subject: [PATCH 4/4] refactor to be cleaner (IMO). refactored a lot of code to clean it up. * changed linkedlist implementation * no longer takes multiple elements, easy enough to either 1) add a new function that is a multi push/unshift 2) just put that logic on the user the reason for this change is to make the WaitQueue's listener removal / count logic simpler. With only one element added, easy to return a "removable" node. This removable node can be removed at tieout time, removing "real" non waiting promises from the listener queue. * with the above, added a remove() member to linkedlist that takes the returned node and removes it from anywhere in the list * change the implementation of linkedlist to use a list head implementation * adjusts the iterator for new linked list format * removed multi push/unshift tests * updated waitqueue to benefit from linkedlist changes * no longer leaving in listeners until they are "flushed out of the list, get removed right away - no longer need to do math to know how many listeners are waiting * for iterator, return the underlying linked list's iterator instead of reimplementing it --- dist/index.d.ts | 11 ++- dist/index.js | 82 ++++--------------- dist/libs/LinkedList.d.ts | 28 ++++--- dist/libs/LinkedList.js | 132 ++++++++++++++---------------- src/index.ts | 61 ++++++-------- src/libs/LinkedList.ts | 164 ++++++++++++++++++++++---------------- tests/LinkedList.test.ts | 52 ++++++------ tests/WaitQueue.test.ts | 41 +++++----- tests/require.test.ts | 1 + 9 files changed, 265 insertions(+), 307 deletions(-) diff --git a/dist/index.d.ts b/dist/index.d.ts index dbf94f4..f9981bd 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -6,16 +6,15 @@ declare class WaitQueue { done: boolean; }; }; - queue: LinkedList; - listeners: LinkedList; + queue: LinkedList; + listeners: LinkedList<(err?: Error | undefined) => unknown>; get length(): number; - numListeners(): number; - numListenersDelta: number; + numWaiters(): number; empty(): void; clear(): void; clearListeners(): void; - unshift(...items: T[]): number; - push(...items: T[]): number; + unshift(item: T): number; + push(item: T): number; private _remove; shift(timeout?: number): Promise; pop(timeout?: number): Promise; diff --git a/dist/index.js b/dist/index.js index 471d38d..38fd695 100644 --- a/dist/index.js +++ b/dist/index.js @@ -10,26 +10,6 @@ var __values = (this && this.__values) || function(o) { }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spread = (this && this.__spread) || function () { - for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); - return ar; -}; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -42,7 +22,6 @@ var WaitQueue = /** @class */ (function () { function WaitQueue() { this.queue = new LinkedList_1.default(); this.listeners = new LinkedList_1.default(); - this.numListenersDelta = 0; } Object.defineProperty(WaitQueue.prototype, "length", { get: function () { @@ -51,8 +30,8 @@ var WaitQueue = /** @class */ (function () { enumerable: false, configurable: true }); - WaitQueue.prototype.numListeners = function () { - return this.listeners.length - this.numListenersDelta; + WaitQueue.prototype.numWaiters = function () { + return this.listeners.length; }; WaitQueue.prototype.empty = function () { this.queue = new LinkedList_1.default(); @@ -76,25 +55,14 @@ var WaitQueue = /** @class */ (function () { finally { if (e_1) throw e_1.error; } } this.listeners = new LinkedList_1.default(); - this.numListenersDelta = 0; }; - WaitQueue.prototype.unshift = function () { - var _a; - var items = []; - for (var _i = 0; _i < arguments.length; _i++) { - items[_i] = arguments[_i]; - } - (_a = this.queue).unshift.apply(_a, __spread(items)); + WaitQueue.prototype.unshift = function (item) { + this.queue.unshift(item); this._flush(); return this.length; }; - WaitQueue.prototype.push = function () { - var _a; - var items = []; - for (var _i = 0; _i < arguments.length; _i++) { - items[_i] = arguments[_i]; - } - (_a = this.queue).push.apply(_a, __spread(items)); + WaitQueue.prototype.push = function (item) { + this.queue.push(item); this._flush(); return this.length; }; @@ -111,33 +79,27 @@ var WaitQueue = /** @class */ (function () { } return new Promise(function (resolve, reject) { var self = _this; - if (_this.queue.length > 0) { + if (self.queue.length > 0) { return resolve(fn()); } else { - var timedOut_1 = false; - var timerId_1 = (timeout && timeout > 0) ? - setTimeout(function () { - self.numListenersDelta++; - timedOut_1 = true; - timerId_1 = undefined; - reject(new Error("Timed Out")); - }, timeout) : undefined; - _this.listeners.push(function (err) { + var timerId_1; + var listener_1 = self.listeners.push(function (err) { if (timerId_1) { clearTimeout(timerId_1); timerId_1 = undefined; } - if (timedOut_1) { - self.numListenersDelta--; - // already rejected, doesn't matter if err via clearListeners - return; - } if (err) { return reject(err); } return resolve(fn()); }); + timerId_1 = (timeout && timeout > 0) ? + setTimeout(function () { + timerId_1 = undefined; + self.listeners.remove(listener_1); + reject(new Error("Timed Out")); + }, timeout) : undefined; } }); }; @@ -157,19 +119,7 @@ var WaitQueue = /** @class */ (function () { }()); if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { WaitQueue.prototype[Symbol.iterator] = function () { - var node = this.queue._front; - return { - next: function () { - if (node === null) { - return { value: null, done: true }; - } - else { - var r = { value: node.item, done: false }; - node = node._next; - return r; - } - }, - }; + return this.queue[Symbol.iterator](); }; } module.exports = WaitQueue; diff --git a/dist/libs/LinkedList.d.ts b/dist/libs/LinkedList.d.ts index d30bb65..c3b6ce3 100644 --- a/dist/libs/LinkedList.d.ts +++ b/dist/libs/LinkedList.d.ts @@ -1,23 +1,25 @@ -interface Node { - _next: Node | null; - _prev: Node | null; - item: any; +interface Node { + _next: Node; + _prev: Node; + _removed?: boolean; + item: T; } -declare class LinkedList { +declare class LinkedList { [Symbol.iterator]: () => { next: () => { - value: any; + value: T; done: boolean; }; }; _length: number; - _front: Node | null; - _end: Node | null; - get length(): number; + _head: Node; + constructor(); empty(): void; - push(...items: any[]): number; - unshift(...items: any[]): number; - pop(): any; - shift(): any; + get length(): number; + push(item: T): Node; + unshift(item: T): Node; + pop(): T; + shift(): T; + remove(node: Node): void; } export default LinkedList; diff --git a/dist/libs/LinkedList.js b/dist/libs/LinkedList.js index 5e1e4f0..9d25812 100644 --- a/dist/libs/LinkedList.js +++ b/dist/libs/LinkedList.js @@ -1,18 +1,31 @@ "use strict"; +/* + * Javascript WaitQueue Object in ES5 + * https://github.com/flarestart/wait-queue-es5 + */ Object.defineProperty(exports, "__esModule", { value: true }); function createNode(item) { - return { + var tmp = { _next: null, _prev: null, item: item }; + tmp._next = tmp; + tmp._prev = tmp; + return tmp; } var LinkedList = /** @class */ (function () { + /* same as empty */ function LinkedList() { this._length = 0; - this._front = null; - this._end = null; + this._head = createNode(); + this._length = 0; } + /* same as constructor */ + LinkedList.prototype.empty = function () { + this._head = createNode(); + this._length = 0; + }; Object.defineProperty(LinkedList.prototype, "length", { get: function () { return this._length; @@ -20,96 +33,71 @@ var LinkedList = /** @class */ (function () { enumerable: false, configurable: true }); - LinkedList.prototype.empty = function () { - this._length = 0; - this._front = null; - this._end = null; - }; - LinkedList.prototype.push = function () { - var _this = this; - var items = []; - for (var _i = 0; _i < arguments.length; _i++) { - items[_i] = arguments[_i]; + LinkedList.prototype.push = function (item) { + var node = createNode(item); + node._next = this._head; + node._prev = this._head._prev; + this._head._prev._next = node; + this._head._prev = node; + if (this._head._next == this._head) { + this._head._next = node; } - items.forEach(function (item) { - var node = createNode(item); - if (_this._end === null) { - _this._front = node; - _this._end = node; - } - else { - _this._end._next = node; - node._prev = _this._end; - _this._end = node; - } - _this._length++; - }); - return this._length; + this._length++; + return node; }; - LinkedList.prototype.unshift = function () { - var _this = this; - var items = []; - for (var _i = 0; _i < arguments.length; _i++) { - items[_i] = arguments[_i]; + LinkedList.prototype.unshift = function (item) { + var node = createNode(item); + node._prev = this._head; + node._next = this._head._next; + this._head._next._prev = node; + this._head._next = node; + if (this._head._prev == this._head) { + this._head._prev = node; } - items.forEach(function (item) { - var node = createNode(item); - if (_this._front === null) { - _this._front = node; - _this._end = node; - } - else { - node._next = _this._front; - _this._front._prev = node; - _this._front = node; - } - _this._length++; - }); - return this._length; + this._length++; + return node; }; LinkedList.prototype.pop = function () { - var item = this._end; - if (item === null) { - return null; - } - if (item._prev != null) { - this._end = item._prev; - this._end._next = null; - } - else { - this._front = null; - this._end = null; + if (this._head._prev == this._head) { + throw new Error("empty list"); } + var item = this._head._prev; + this._head._prev = item._prev; + item._prev._next = this._head; + item._removed = true; this._length--; - item._prev = null; return item.item; }; LinkedList.prototype.shift = function () { - var item = this._front; - if (item === null) { - return null; - } - if (item._next != null) { - this._front = item._next; - this._front._prev = null; + if (this._head._next == this._head) { + throw new Error("empty list"); } - else { - this._front = null; - this._end = null; - } - item._next = null; + var item = this._head._next; + this._head._next = item._next; + item._next._prev = this._head; + item._removed = true; this._length--; return item.item; }; + LinkedList.prototype.remove = function (node) { + if (node._removed) { + return; + } + node._prev._next = node._next; + node._next._prev = node._prev; + node._removed = true; + this._length--; + }; return LinkedList; }()); /* istanbul ignore next */ if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { LinkedList.prototype[Symbol.iterator] = function () { - var node = this._front; + var head = this._head; + var node = this._head._next; return { next: function () { - if (node === null) { + if (node === head) { return { value: null, done: true }; } var r = { value: node.item, done: false }; diff --git a/src/index.ts b/src/index.ts index 584b598..4534389 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,17 +7,16 @@ import LinkedList from './libs/LinkedList'; class WaitQueue { [Symbol.iterator]: () => { next: () => { value: any; done: boolean } }; - queue = new LinkedList(); - listeners = new LinkedList(); + queue = new LinkedList(); + listeners = new LinkedList<(err?: Error) => unknown>(); get length(): number { return this.queue.length; } - numListeners(): number { - return this.listeners.length - this.numListenersDelta; + numWaiters(): number { + return this.listeners.length; } - numListenersDelta = 0; empty(): void { this.queue = new LinkedList(); @@ -25,20 +24,22 @@ class WaitQueue { clear(): void { this.queue = new LinkedList(); } + clearListeners(): void { for (const listener of this.listeners) { listener(new Error('Clear Listeners')); } this.listeners = new LinkedList(); - this.numListenersDelta = 0; } - unshift(...items: T[]): number { - this.queue.unshift(...items); + + unshift(item: T): number { + this.queue.unshift(item); this._flush(); return this.length; } - push(...items: T[]): number { - this.queue.push(...items); + + push(item: T): number { + this.queue.push(item); this._flush(); return this.length; } @@ -56,36 +57,31 @@ class WaitQueue { return new Promise((resolve, reject) => { const self = this; - if (this.queue.length > 0) { + + if (self.queue.length > 0) { return resolve(fn()); } else { - let timedOut = false; - let timerId = (timeout && timeout > 0) ? - setTimeout(() => { - self.numListenersDelta++; - timedOut = true; - timerId = undefined; - reject(new Error("Timed Out")); - }, timeout) : undefined; + let timerId: NodeJS.Timeout | undefined; - this.listeners.push((err: Error) => { + const listener = self.listeners.push((err?: Error) => { if (timerId) { clearTimeout(timerId); timerId = undefined; } - if (timedOut) { - self.numListenersDelta--; - // already rejected, doesn't matter if err via clearListeners - return; - } - if (err) { return reject(err); } return resolve(fn()); }); + + timerId = (timeout && timeout > 0) ? + setTimeout(() => { + timerId = undefined; + self.listeners.remove(listener); + reject(new Error("Timed Out")); + }, timeout) : undefined; } }); } @@ -107,18 +103,7 @@ class WaitQueue { if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { WaitQueue.prototype[Symbol.iterator] = function () { - let node = this.queue._front; - return { - next() { - if (node === null) { - return { value: null, done: true }; - } else { - const r = { value: node.item, done: false }; - node = node._next; - return r; - } - }, - }; + return this.queue[Symbol.iterator](); }; } diff --git a/src/libs/LinkedList.ts b/src/libs/LinkedList.ts index 395dd9e..3118ebc 100644 --- a/src/libs/LinkedList.ts +++ b/src/libs/LinkedList.ts @@ -2,107 +2,133 @@ * Javascript WaitQueue Object in ES5 * https://github.com/flarestart/wait-queue-es5 */ -interface Node { - _next: Node | null; - _prev: Node | null; - item: any; + +interface Node { + _next: Node; + _prev: Node; + _removed?: boolean; + item: T } -function createNode(item: any): Node { - return { +function createNode(item?: T): Node { + const tmp : { + _next: Node | null; + _prev: Node | null; + item?: T +} = { _next: null, _prev: null, - item - }; + item: item + } + + tmp._next = tmp as Node; + tmp._prev = tmp as Node; + + return tmp as Node; } -class LinkedList { - [Symbol.iterator]: () => { next: () => { value: any; done: boolean } }; +class LinkedList { + [Symbol.iterator]: () => { next: () => { value: T; done: boolean } }; _length = 0; - _front: Node | null = null; - _end: Node | null = null; + _head: Node; - get length() { - return this._length; + /* same as empty */ + constructor() { + this._head = createNode(); + this._length = 0; } + + /* same as constructor */ empty() { + this._head = createNode(); this._length = 0; - this._front = null; - this._end = null; } - push(...items: any[]) { - items.forEach(item => { - const node = createNode(item); - if (this._end === null) { - this._front = node; - this._end = node; - } else { - this._end._next = node; - node._prev = this._end; - this._end = node; - } - this._length++; - }); + + get length() { return this._length; } - unshift(...items: any[]) { - items.forEach(item => { - const node = createNode(item); - if (this._front === null) { - this._front = node; - this._end = node; - } else { - node._next = this._front; - this._front._prev = node; - this._front = node; - } - this._length++; - }); - return this._length; + + push(item: T) { + const node = createNode(item); + + node._next = this._head; + node._prev = this._head._prev; + this._head._prev._next = node; + this._head._prev = node; + if (this._head._next == this._head) { + this._head._next = node; + } + this._length++; + + return node; } - pop() { - const item = this._end; - if (item === null) { - return null; + + unshift(item: T) { + const node = createNode(item); + + node._prev = this._head; + node._next = this._head._next; + this._head._next._prev = node; + this._head._next = node; + if (this._head._prev == this._head) { + this._head._prev = node; } - if (item._prev != null) { - this._end = item._prev; - this._end._next = null; - } else { - this._front = null; - this._end = null; + this._length++; + + return node; + } + + pop() { + if (this._head._prev == this._head) { + throw new Error("empty list") } + + const item = this._head._prev; + this._head._prev = item._prev; + item._prev._next = this._head + + item._removed = true; this._length--; - item._prev = null; - return item.item; + + return item.item } + shift() { - const item = this._front; - if (item === null) { - return null; - } - if (item._next != null) { - this._front = item._next; - this._front._prev = null; - } else { - this._front = null; - this._end = null; + if (this._head._next == this._head) { + throw new Error("empty list") } - item._next = null; + + const item = this._head._next; + this._head._next = item._next + item._next._prev = this._head; + + item._removed = true; this._length--; - return item.item; + + return item.item } + remove(node: Node) { + if (node._removed) { + return; + } + node._prev._next = node._next; + node._next._prev = node._prev; + + node._removed = true; + this._length--; + } } /* istanbul ignore next */ if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { LinkedList.prototype[Symbol.iterator] = function() { - let node = this._front; + const head = this._head; + let node = this._head._next; return { next() { - if (node === null) { + if (node === head) { return { value: null, done: true }; } const r = { value: node.item, done: false }; diff --git a/tests/LinkedList.test.ts b/tests/LinkedList.test.ts index 453999f..fd29adb 100644 --- a/tests/LinkedList.test.ts +++ b/tests/LinkedList.test.ts @@ -3,16 +3,19 @@ import LinkedList from '../src/libs/LinkedList'; describe('LinkedList', function() { const ll = new LinkedList(); + beforeEach(function() { // clear waitqueue ll.empty(); }); + it('length should equal to 10', function() { for (let n = 0; n < 10; n++) { ll.push(n); } assert.deepStrictEqual(10, ll.length); }); + it('set length will throw an error in strict mode', function() { assert.throws(function() { 'use strict'; @@ -21,61 +24,62 @@ describe('LinkedList', function() { obj.length = 10; }, /Cannot set property/); }); + it('empty()', function() { ll.push(1); ll.empty(); - assert.strictEqual(null, ll._front); - assert.strictEqual(null, ll._end); + assert.strictEqual(ll._head._next, ll._head); + assert.strictEqual(ll._head._prev, ll._head); assert.strictEqual(0, ll.length); }); + it('push 10 times', function() { for (let n = 0; n < 10; n++) { ll.push(n); } - assert.deepStrictEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], Array.from(ll)); + assert.deepStrictEqual(Array.from(ll), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); + it('push() should be sequence', function() { for (let n = 0; n < 5; n++) { ll.push(n); } - assert.deepStrictEqual([0, 1, 2, 3, 4], Array.from(ll)); - }); - it('push() can receive multi args', function() { - ll.push(0, 1, 2, 3, 4); - assert.deepStrictEqual([0, 1, 2, 3, 4], Array.from(ll)); - }); - it('unshift() should return queue length', function() { - assert.strictEqual(1, ll.unshift(1)); + assert.deepStrictEqual(Array.from(ll), [0, 1, 2, 3, 4]); }); + it('unshift() should be reverse', function() { for (let n = 0; n < 5; n++) { ll.unshift(n); } - assert.deepStrictEqual([4, 3, 2, 1, 0], Array.from(ll)); - }); - it('unshift() can receive multi args', function() { - ll.unshift(0, 1, 2, 3, 4); - assert.deepStrictEqual([4, 3, 2, 1, 0], Array.from(ll)); + assert.deepStrictEqual(Array.from(ll), [4, 3, 2, 1, 0]); }); - it('shift() should return null', function() { - const ret = ll.shift(); - assert.equal(ret, null); + it('shift() should return error', function() { + try { + ll.shift(); + assert.equal(true, false); + } catch (err) { + assert.deepStrictEqual(err, new Error("empty list")); + } }); - it('pop() should return null', function() { - const ret = ll.pop(); - assert.equal(ret, null); + it('pop() should return error', function() { + try { + ll.pop(); + assert.equal(true, false); + } catch (err) { + assert.deepStrictEqual(err, new Error("empty list")); + } }); - it('shift() should return null', function() { + it('shift() should return value', function() { ll.push('step1'); ll.push('step2'); const ret = ll.shift(); assert.equal(ret, 'step1'); }); - it('pop() should return null', function() { + it('pop() should return value', function() { ll.push('step1'); ll.push('step2'); const ret = ll.pop(); diff --git a/tests/WaitQueue.test.ts b/tests/WaitQueue.test.ts index 40acc56..b452970 100644 --- a/tests/WaitQueue.test.ts +++ b/tests/WaitQueue.test.ts @@ -3,17 +3,20 @@ import WaitQueue from '../src/index'; describe('Methods of WaitQueue', function() { const wq = new WaitQueue(); + beforeEach(function() { // clear waitqueue wq.clear(); wq.clearListeners(); }); + it('length is equal to 10', function() { for (let n = 0; n < 10; n++) { wq.push(n); } assert.deepStrictEqual(10, wq.length); }); + it('set length will throw an error in strict mode', function() { assert.throws(function() { 'use strict'; @@ -22,11 +25,13 @@ describe('Methods of WaitQueue', function() { obj.length = 10; }, /Cannot set property/); }); + it('empty()', function() { wq.push(1); wq.empty(); assert.deepStrictEqual([], Array.from(wq)); }); + it('clearListeners() will send error to wait listeners', function(done) { const w = new WaitQueue(); let num = 0; @@ -43,36 +48,30 @@ describe('Methods of WaitQueue', function() { w.pop().catch(handler); w.clearListeners(); }); + it('push() should return queue length', function() { assert.strictEqual(1, wq.push(1)); }); + it('push() should be sequence', function() { for (let n = 0; n < 5; n++) { wq.push(n); } assert.deepStrictEqual([0, 1, 2, 3, 4], Array.from(wq)); }); - it('push() can receive multi args', function() { - wq.push(0, 1, 2, 3, 4); - assert.deepStrictEqual([0, 1, 2, 3, 4], Array.from(wq)); - }); - it('unshift() should return queue length', function() { - assert.strictEqual(1, wq.unshift(1)); - }); + it('unshift() should be reverse', function() { for (let n = 0; n < 5; n++) { wq.unshift(n); } assert.deepStrictEqual([4, 3, 2, 1, 0], Array.from(wq)); }); - it('unshift() can receive multi args', function() { - wq.unshift(0, 1, 2, 3, 4); - assert.deepStrictEqual([4, 3, 2, 1, 0], Array.from(wq)); - }); + it('shift() should return a promise', function() { wq.push(1); assert.ok(wq.shift() instanceof Promise); }); + it('pop() should return a promise', function() { wq.push(1); assert.ok(wq.pop() instanceof Promise); @@ -101,14 +100,18 @@ describe('Methods of WaitQueue', function() { }); it('multiple shifts are in correct order', async function() { - wq.push(0, 1, 2, 3, 4); + for (let i=0; i < 5; i++) { + wq.push(i); + } for(let i=0; i < 5; i++) { assert.strictEqual(await wq.shift(), i) } }) it('multiple pops are in correct order', async function() { - wq.push(0, 1, 2, 3, 4); + for (let i=0; i < 5; i++) { + wq.push(i); + } for(let i=4; i >= 0; i--) { assert.strictEqual(await wq.pop(), i) } @@ -131,19 +134,19 @@ describe('Methods of WaitQueue', function() { assert.strictEqual(await p2, 1); }) - it('count number of listeners', async function() { + it('count number of waiting promises', async function() { const p1 = wq.pop(1); const p2 = wq.pop(); - wq.pop().catch(() => {}); // this will be cleared, and hence failed; + wq.pop().catch(() => {}); // this will be cleared, so ignore error - assert.strictEqual(wq.numListeners(), 3); - setTimeout(() => assert.strictEqual(wq.numListeners(), 2), 10); + assert.strictEqual(wq.numWaiters(), 3); + setTimeout(() => assert.strictEqual(wq.numWaiters(), 2), 10); setTimeout(() => wq.push(1), 20); assert.rejects(p1); assert.strictEqual(await p2, 1); - assert.strictEqual(wq.numListeners(), 1) + assert.strictEqual(wq.numWaiters(), 1) wq.clearListeners(); - assert.strictEqual(wq.numListeners(), 0); + assert.strictEqual(wq.numWaiters(), 0); }) it('Iterator for(... of ...)', function() { diff --git a/tests/require.test.ts b/tests/require.test.ts index e136e8d..9f562bd 100644 --- a/tests/require.test.ts +++ b/tests/require.test.ts @@ -5,6 +5,7 @@ describe('require WaitQueue', function () { it('expect WaitQueue to be a function', function () { expect(typeof WaitQueue).toEqual('function'); }); + it('new WaitQueue', function () { expect(new WaitQueue()).toBeInstanceOf(WaitQueue); });