diff --git a/JavaScript/.d.ts b/JavaScript/.d.ts new file mode 100644 index 0000000..aa8e4e0 --- /dev/null +++ b/JavaScript/.d.ts @@ -0,0 +1,22 @@ +declare class TimeoutCollection { + constructor(timeout: number); + + set(key: K, value: V): this; + get(key: K): V | undefined; + has(key: K): boolean; + delete(key: K): boolean; + clear(): void; + size(): number; + toArray(): [K, V][]; + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void; + keys(): IterableIterator; + values(): IterableIterator; + entries(): IterableIterator<[K, V]>; + [Symbol.iterator](): IterableIterator<[K, V]>; + + private collection: Map; + private timers: Map; + private timeout: number; +} + +export = TimeoutCollection; diff --git a/JavaScript/1-prototype.js b/JavaScript/1-prototype.js index fbec828..5cc6d07 100644 --- a/JavaScript/1-prototype.js +++ b/JavaScript/1-prototype.js @@ -3,55 +3,93 @@ // Facade that wraps Map and Node.js Timers to provide a simple interface for a // collection with values that have expiration timeout. -const TimeoutCollection = function (timeout) { - this.timeout = timeout; - this.collection = new Map(); - this.timers = new Map(); -}; - -TimeoutCollection.prototype.set = function (key, value) { - const timer = this.timers.get(key); - if (timer) clearTimeout(timer); - const timeout = setTimeout(() => { +class TimeoutCollection { + constructor(timeout) { + this.timeout = timeout; + this.collection = new Map(); + this.timers = new Map(); + this.isNode = typeof process !== 'undefined' && process.versions?.node; + } + + set(key, value) { this.delete(key); - }, this.timeout); - timeout.unref(); - this.collection.set(key, value); - this.timers.set(key, timeout); -}; -TimeoutCollection.prototype.get = function (key) { - return this.collection.get(key); -}; + const timeoutId = setTimeout(() => { + this.delete(key); + }, this.timeout); + + if (this.isNode && typeof timeoutId.unref === 'function') { + timeoutId.unref(); + } -TimeoutCollection.prototype.delete = function (key) { - const timer = this.timers.get(key); - if (timer) { - clearTimeout(timer); - this.collection.delete(key); - this.timers.delete(key); + this.collection.set(key, value); + this.timers.set(key, timeoutId); } -}; -TimeoutCollection.prototype.toArray = function () { - return [...this.collection.entries()]; -}; + get(key) { + return this.collection.get(key); + } -// Usage + delete(key) { + const timer = this.timers.get(key); + if (timer) { + clearTimeout(timer); + this.timers.delete(key); + this.collection.delete(key); + return true; + } + return false; + } + + clear() { + for (const timer of this.timers.values()) { + clearTimeout(timer); + } + this.timers.clear(); + this.collection.clear(); + } + + has(key) { + return this.collection.has(key); + } -const hash = new TimeoutCollection(1000); -hash.set('uno', 1); -console.dir({ array: hash.toArray() }); + size() { + return this.collection.size; + } + + toArray() { + return [...this.collection.entries()]; + } + + destroy() { + this.clear(); + } +} -hash.set('due', 2); -console.dir({ array: hash.toArray() }); +if (typeof module !== 'undefined' && module.exports) { + module.exports = TimeoutCollection; +} else { + window.TimeoutCollection = TimeoutCollection; +} -setTimeout(() => { - hash.set('tre', 3); +// Usage + +if (typeof process !== 'undefined' && process.versions?.node) { + const hash = new TimeoutCollection(1000); + hash.set('uno', 1); + console.dir({ array: hash.toArray() }); + + hash.set('due', 2); console.dir({ array: hash.toArray() }); setTimeout(() => { - hash.set('quattro', 4); + hash.set('tre', 3); console.dir({ array: hash.toArray() }); - }, 500); -}, 1500); + + setTimeout(() => { + hash.set('quattro', 4); + console.dir({ array: hash.toArray() }); + hash.destroy(); + }, 500); + }, 1500); +} diff --git a/JavaScript/2-class.js b/JavaScript/2-class.js index 094cf18..fee1b1b 100644 --- a/JavaScript/2-class.js +++ b/JavaScript/2-class.js @@ -13,31 +13,68 @@ class TimeoutCollection { const timeout = setTimeout(() => { this.delete(key); }, this.timeout); - timeout.unref(); + if (typeof timeout.unref === 'function') timeout.unref(); this.collection.set(key, value); this.timers.set(key, timeout); + return this; } get(key) { return this.collection.get(key); } + has(key) { + return this.collection.has(key); + } + delete(key) { const timer = this.timers.get(key); if (timer) { clearTimeout(timer); - this.collection.delete(key); this.timers.delete(key); + return this.collection.delete(key); + } + return false; + } + + clear() { + for (const timer of this.timers.values()) { + clearTimeout(timer); } + this.timers.clear(); + this.collection.clear(); + } + + size() { + return this.collection.size; } toArray() { return [...this.collection.entries()]; } + + forEach(callbackfn, thisArg) { + return this.collection.forEach(callbackfn, thisArg); + } + + keys() { + return this.collection.keys(); + } + + values() { + return this.collection.values(); + } + + entries() { + return this.collection.entries(); + } + + [Symbol.iterator]() { + return this.collection[Symbol.iterator](); + } } // Usage - const hash = new TimeoutCollection(1000); hash.set('uno', 1); console.dir({ array: hash.toArray() }); diff --git a/JavaScript/3-function.js b/JavaScript/3-function.js index 89821ea..da714ba 100644 --- a/JavaScript/3-function.js +++ b/JavaScript/3-function.js @@ -2,39 +2,40 @@ const timeoutCollection = (interval) => { const collection = new Map(); - const timers = new Map(); - - const instance = {}; - - instance.set = (key, value) => { - const timer = timers.get(key); - if (timer) clearTimeout(timer); - const timeout = setTimeout(() => { + const expirations = new Map(); + + return { + set(key, value) { + const expiration = Date.now() + interval; + collection.set(key, value); + expirations.set(key, expiration); + return this; + }, + get(key) { + this.cleanup(); + return collection.get(key); + }, + delete(key) { collection.delete(key); - }, interval); - timeout.unref(); - collection.set(key, value); - timers.set(key, timer); - }; - - instance.get = (key) => collection.get(key); - - instance.delete = (key) => { - const timer = timers.get(key); - if (timer) { - clearTimeout(timer); - collection.delete(key); - timers.delete(key); + expirations.delete(key); + }, + toArray() { + this.cleanup(); + return [...collection.entries()]; + }, + cleanup() { + const now = Date.now(); + for (const [key, expiration] of expirations.entries()) { + if (now >= expiration) { + collection.delete(key); + expirations.delete(key); + } + } } }; - - instance.toArray = () => [...collection.entries()]; - - return instance; }; // Usage - const hash = timeoutCollection(1000); hash.set('uno', 1); console.dir({ array: hash.toArray() }); @@ -49,5 +50,6 @@ setTimeout(() => { setTimeout(() => { hash.set('quattro', 4); console.dir({ array: hash.toArray() }); + hash.cleanup(); // Manually trigger cleanup }, 500); }, 1500); diff --git a/JavaScript/4-scheduler.js b/JavaScript/4-scheduler.js index 953c9f5..9433caf 100644 --- a/JavaScript/4-scheduler.js +++ b/JavaScript/4-scheduler.js @@ -3,14 +3,24 @@ const { EventEmitter } = require('node:events'); class Logger { - static color(level) { + static COLORS = { + warn: '\x1b[1;33m', + error: '\x1b[0;31m', + info: '\x1b[1;37m', + }; + + constructor(output = console) { + this.output = output; + } + + color(level) { return Logger.COLORS[level] || Logger.COLORS.info; } log(level, s) { const date = new Date().toISOString(); - const color = Logger.color(level); - console.log(color + date + '\t' + s + '\x1b[0m'); + const color = this.color(level); + this.output.log(color + date + '\t' + s + '\x1b[0m'); } warn(s) { @@ -26,25 +36,10 @@ class Logger { } } -Logger.COLORS = { - warn: '\x1b[1;33m', - error: '\x1b[0;31m', - info: '\x1b[1;37m', -}; - class Task extends EventEmitter { - constructor(name, time, exec) { + constructor(name, exec) { super(); this.name = name; - if (typeof time === 'number') { - this.time = Date.now() + time; - this.set = setInterval; - this.clear = clearInterval; - } else { - this.time = new Date(time).getTime(); - this.set = setTimeout; - this.clear = clearTimeout; - } this.exec = exec; this.running = false; this.count = 0; @@ -87,26 +82,46 @@ class Task extends EventEmitter { } } +class IntervalTask extends Task { + constructor(name, time, exec) { + super(name, exec); + this.time = Date.now() + time; + this.set = setInterval; + this.clear = clearInterval; + } +} + +class TimeoutTask extends Task { + constructor(name, time, exec) { + super(name, exec); + this.time = new Date(time).getTime(); + this.set = setTimeout; + this.clear = clearTimeout; + } +} + class Scheduler extends EventEmitter { - constructor() { + constructor({ options = { output: console } } = {}) { super(); this.tasks = new Map(); - this.logger = new Logger(); + this.logger = new Logger(options.output); } task(name, time, exec) { this.stop(name); - const task = new Task(name, time, exec); + const task = typeof time === 'number' + ? new IntervalTask(name, time, exec) + : new TimeoutTask(name, time, exec); this.tasks.set(name, task); task.on('error', (err) => { - this.logger.error(task.name + '\t' + err.message); + this.logger.error(`${task.name}\t${err.message}`); this.emit('error', err, task); }); task.on('begin', () => { - this.logger.info(task.name + '\tbegin'); + this.logger.info(`${task.name}\tbegin`); }); task.on('end', (res = '') => { - this.logger.warn(task.name + '\tend\t' + res); + this.logger.warn(`${task.name}\tend\t${res}`); }); task.start(); return task; @@ -128,21 +143,20 @@ class Scheduler extends EventEmitter { } // Usage - -const scheduler = new Scheduler(); +const scheduler = new Scheduler({ options: { output: console } }); scheduler.on('error', (err, task) => { console.log(`Error in ${task.name}:\n ${err.stack}`); //process.exit(1); }); -scheduler.task('name1', '2019-03-12T14:37Z', (done) => { +scheduler.task('name1', '2035-03-12T14:37Z', (done) => { setTimeout(() => { done(null, 'task successed'); }, 1000); }); -scheduler.task('name2', '2019-03-12T14:37Z', (done) => { +scheduler.task('name2', '2035-03-12T14:37Z', (done) => { setTimeout(() => { done(new Error('task failed')); }, 1100);