architex-js 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/cache/Cache.js +115 -0
- package/src/cache/index.js +1 -0
- package/src/index.js +3 -1
- package/src/scheduler/Scheduler.js +100 -0
- package/src/scheduler/index.js +1 -0
- package/test/cache.test.js +88 -0
- package/test/scheduler.test.js +81 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "architex-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.js",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"./guards": "./src/guards/index.js",
|
|
14
14
|
"./repository": "./src/repository/index.js",
|
|
15
15
|
"./events": "./src/events/index.js",
|
|
16
|
-
"./cqrs": "./src/cqrs/index.js"
|
|
16
|
+
"./cqrs": "./src/cqrs/index.js",
|
|
17
|
+
"./scheduler": "./src/scheduler/index.js",
|
|
18
|
+
"./cache": "./src/cache/index.js"
|
|
17
19
|
},
|
|
18
20
|
"description": "Architectural Toolbox for JavaScript - Providing high-level building blocks for robust systems.",
|
|
19
21
|
"author": {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache with TTL (time-to-live), lazy expiry, and `remember()` helper.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const cache = new Cache({ ttl: 60000 }); // 60 seconds default TTL
|
|
6
|
+
* const user = await cache.remember('user:1', () => fetchUser(1));
|
|
7
|
+
*/
|
|
8
|
+
class Cache {
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ ttl?: number }} [options]
|
|
11
|
+
* @param {number} [options.ttl=0] - Default TTL in milliseconds. 0 = no expiry.
|
|
12
|
+
*/
|
|
13
|
+
constructor({ ttl = 0 } = {}) {
|
|
14
|
+
/** @type {number} Default TTL in ms */
|
|
15
|
+
this._defaultTtl = ttl;
|
|
16
|
+
/** @type {Map<string, { value: any, expiresAt: number | null }>} */
|
|
17
|
+
this._store = new Map();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stores a value in the cache.
|
|
22
|
+
* @param {string} key
|
|
23
|
+
* @param {any} value
|
|
24
|
+
* @param {number} [ttl] - TTL in milliseconds. Falls back to the default. 0 = no expiry.
|
|
25
|
+
*/
|
|
26
|
+
set(key, value, ttl = this._defaultTtl) {
|
|
27
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : null;
|
|
28
|
+
this._store.set(key, { value, expiresAt });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves a value. Returns `undefined` if the key doesn't exist or has expired.
|
|
33
|
+
* @param {string} key
|
|
34
|
+
* @returns {any | undefined}
|
|
35
|
+
*/
|
|
36
|
+
get(key) {
|
|
37
|
+
const entry = this._store.get(key);
|
|
38
|
+
if (!entry) return undefined;
|
|
39
|
+
|
|
40
|
+
if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
|
|
41
|
+
this._store.delete(key);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return entry.value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the key exists and has not expired.
|
|
50
|
+
* @param {string} key
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
has(key) {
|
|
54
|
+
return this.get(key) !== undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes a key from the cache.
|
|
59
|
+
* @param {string} key
|
|
60
|
+
*/
|
|
61
|
+
delete(key) {
|
|
62
|
+
this._store.delete(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Clears all entries in the cache.
|
|
67
|
+
*/
|
|
68
|
+
clear() {
|
|
69
|
+
this._store.clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the cached value if it exists; otherwise calls the factory,
|
|
74
|
+
* caches the result, and returns it. (Cache-aside pattern)
|
|
75
|
+
* @template T
|
|
76
|
+
* @param {string} key
|
|
77
|
+
* @param {() => T | Promise<T>} factory - Function that generates the value if cache misses.
|
|
78
|
+
* @param {number} [ttl] - Optional TTL override.
|
|
79
|
+
* @returns {Promise<T>}
|
|
80
|
+
*/
|
|
81
|
+
async remember(key, factory, ttl = this._defaultTtl) {
|
|
82
|
+
const cached = this.get(key);
|
|
83
|
+
if (cached !== undefined) return cached;
|
|
84
|
+
|
|
85
|
+
const value = await factory();
|
|
86
|
+
this.set(key, value, ttl);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the number of entries in the cache (including potentially expired ones).
|
|
92
|
+
* @returns {number}
|
|
93
|
+
*/
|
|
94
|
+
get size() {
|
|
95
|
+
return this._store.size;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Purges all expired entries.
|
|
100
|
+
* @returns {number} Number of entries removed.
|
|
101
|
+
*/
|
|
102
|
+
purgeExpired() {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
let removed = 0;
|
|
105
|
+
for (const [key, entry] of this._store) {
|
|
106
|
+
if (entry.expiresAt !== null && now >= entry.expiresAt) {
|
|
107
|
+
this._store.delete(key);
|
|
108
|
+
removed++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return removed;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { Cache };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Cache.js";
|
package/src/index.js
CHANGED
|
@@ -7,4 +7,6 @@ export * from "./result/index.js";
|
|
|
7
7
|
export * from "./guards/index.js";
|
|
8
8
|
export * from "./repository/index.js";
|
|
9
9
|
export * from "./events/index.js";
|
|
10
|
-
export * from "./cqrs/index.js";
|
|
10
|
+
export * from "./cqrs/index.js";
|
|
11
|
+
export * from "./scheduler/index.js";
|
|
12
|
+
export * from "./cache/index.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages scheduled tasks (repeating intervals and one-time delays).
|
|
3
|
+
* All registered tasks can be cleared at once.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const scheduler = new Scheduler();
|
|
7
|
+
* const stop = scheduler.every(5000, () => console.log('tick'));
|
|
8
|
+
* stop(); // cancel just this task
|
|
9
|
+
*
|
|
10
|
+
* scheduler.clear(); // cancel all tasks
|
|
11
|
+
*/
|
|
12
|
+
class Scheduler {
|
|
13
|
+
constructor() {
|
|
14
|
+
/** @type {Set<any>} */
|
|
15
|
+
this._timers = new Set();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runs a callback repeatedly every `ms` milliseconds.
|
|
20
|
+
* @param {number} ms - Interval in milliseconds.
|
|
21
|
+
* @param {() => void | Promise<void>} callback
|
|
22
|
+
* @returns {() => void} A cancellation function.
|
|
23
|
+
*/
|
|
24
|
+
every(ms, callback) {
|
|
25
|
+
if (typeof ms !== 'number' || ms <= 0) {
|
|
26
|
+
throw new TypeError('Scheduler.every() expects a positive number of milliseconds');
|
|
27
|
+
}
|
|
28
|
+
if (typeof callback !== 'function') {
|
|
29
|
+
throw new TypeError('Scheduler.every() expects a function as second argument');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const id = setInterval(async () => {
|
|
33
|
+
try {
|
|
34
|
+
await callback();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`[Scheduler] Error in repeating task:`, e);
|
|
37
|
+
}
|
|
38
|
+
}, ms);
|
|
39
|
+
|
|
40
|
+
this._timers.add(id);
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
clearInterval(id);
|
|
44
|
+
this._timers.delete(id);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs a callback once after `ms` milliseconds.
|
|
50
|
+
* @param {number} ms - Delay in milliseconds.
|
|
51
|
+
* @param {() => void | Promise<void>} callback
|
|
52
|
+
* @returns {() => void} A cancellation function.
|
|
53
|
+
*/
|
|
54
|
+
after(ms, callback) {
|
|
55
|
+
if (typeof ms !== 'number' || ms < 0) {
|
|
56
|
+
throw new TypeError('Scheduler.after() expects a non-negative number of milliseconds');
|
|
57
|
+
}
|
|
58
|
+
if (typeof callback !== 'function') {
|
|
59
|
+
throw new TypeError('Scheduler.after() expects a function as second argument');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const id = setTimeout(async () => {
|
|
63
|
+
try {
|
|
64
|
+
await callback();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error(`[Scheduler] Error in delayed task:`, e);
|
|
67
|
+
} finally {
|
|
68
|
+
this._timers.delete(id);
|
|
69
|
+
}
|
|
70
|
+
}, ms);
|
|
71
|
+
|
|
72
|
+
this._timers.add(id);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(id);
|
|
76
|
+
this._timers.delete(id);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the number of active timers.
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
get size() {
|
|
85
|
+
return this._timers.size;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Cancels all scheduled tasks.
|
|
90
|
+
*/
|
|
91
|
+
clear() {
|
|
92
|
+
for (const id of this._timers) {
|
|
93
|
+
clearInterval(id);
|
|
94
|
+
clearTimeout(id);
|
|
95
|
+
}
|
|
96
|
+
this._timers.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { Scheduler };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Scheduler.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Cache } from '../src/cache/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Cache', () => {
|
|
5
|
+
let cache;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
cache = new Cache({ ttl: 1000 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should set and get a value', () => {
|
|
17
|
+
cache.set('key', 'value');
|
|
18
|
+
expect(cache.get('key')).toBe('value');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return undefined for a missing key', () => {
|
|
22
|
+
expect(cache.get('missing')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should expire entries after TTL', () => {
|
|
26
|
+
cache.set('key', 42, 500);
|
|
27
|
+
vi.advanceTimersByTime(499);
|
|
28
|
+
expect(cache.get('key')).toBe(42);
|
|
29
|
+
|
|
30
|
+
vi.advanceTimersByTime(1);
|
|
31
|
+
expect(cache.get('key')).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should not expire entries with ttl=0', () => {
|
|
35
|
+
const persistent = new Cache({ ttl: 0 });
|
|
36
|
+
persistent.set('forever', 'yes');
|
|
37
|
+
vi.advanceTimersByTime(9999999);
|
|
38
|
+
expect(persistent.get('forever')).toBe('yes');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('has() should return true for existing non-expired keys', () => {
|
|
42
|
+
cache.set('x', 1);
|
|
43
|
+
expect(cache.has('x')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('has() should return false for expired keys', () => {
|
|
47
|
+
cache.set('x', 1, 100);
|
|
48
|
+
vi.advanceTimersByTime(101);
|
|
49
|
+
expect(cache.has('x')).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('delete() should remove a key', () => {
|
|
53
|
+
cache.set('dKey', 'val');
|
|
54
|
+
cache.delete('dKey');
|
|
55
|
+
expect(cache.get('dKey')).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('clear() should remove all entries', () => {
|
|
59
|
+
cache.set('a', 1);
|
|
60
|
+
cache.set('b', 2);
|
|
61
|
+
cache.clear();
|
|
62
|
+
expect(cache.size).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('remember() should call the factory on cache miss', async () => {
|
|
66
|
+
const factory = vi.fn().mockResolvedValue('fetched');
|
|
67
|
+
const result = await cache.remember('r', factory);
|
|
68
|
+
expect(result).toBe('fetched');
|
|
69
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('remember() should not call factory on cache hit', async () => {
|
|
73
|
+
const factory = vi.fn().mockResolvedValue('fetched');
|
|
74
|
+
await cache.remember('r', factory);
|
|
75
|
+
await cache.remember('r', factory);
|
|
76
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('purgeExpired() should remove expired entries and return count', () => {
|
|
80
|
+
cache.set('a', 1, 100);
|
|
81
|
+
cache.set('b', 2, 100);
|
|
82
|
+
cache.set('c', 3, 99999);
|
|
83
|
+
vi.advanceTimersByTime(101);
|
|
84
|
+
const removed = cache.purgeExpired();
|
|
85
|
+
expect(removed).toBe(2);
|
|
86
|
+
expect(cache.has('c')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Scheduler } from '../src/scheduler/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Scheduler', () => {
|
|
5
|
+
let scheduler;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
scheduler = new Scheduler();
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
scheduler.clear();
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('every() should call callback repeatedly', () => {
|
|
18
|
+
const fn = vi.fn();
|
|
19
|
+
scheduler.every(1000, fn);
|
|
20
|
+
|
|
21
|
+
vi.advanceTimersByTime(3000);
|
|
22
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('every() should return a cancel function that stops the interval', () => {
|
|
26
|
+
const fn = vi.fn();
|
|
27
|
+
const stop = scheduler.every(1000, fn);
|
|
28
|
+
|
|
29
|
+
vi.advanceTimersByTime(2000);
|
|
30
|
+
stop();
|
|
31
|
+
vi.advanceTimersByTime(3000);
|
|
32
|
+
|
|
33
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('after() should call callback once after the delay', () => {
|
|
37
|
+
const fn = vi.fn();
|
|
38
|
+
scheduler.after(500, fn);
|
|
39
|
+
|
|
40
|
+
vi.advanceTimersByTime(499);
|
|
41
|
+
expect(fn).not.toHaveBeenCalled();
|
|
42
|
+
|
|
43
|
+
vi.advanceTimersByTime(1);
|
|
44
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('after() cancel function should prevent execution', () => {
|
|
48
|
+
const fn = vi.fn();
|
|
49
|
+
const cancel = scheduler.after(1000, fn);
|
|
50
|
+
|
|
51
|
+
cancel();
|
|
52
|
+
vi.advanceTimersByTime(2000);
|
|
53
|
+
|
|
54
|
+
expect(fn).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('clear() should stop all active timers', () => {
|
|
58
|
+
const fn1 = vi.fn();
|
|
59
|
+
const fn2 = vi.fn();
|
|
60
|
+
scheduler.every(500, fn1);
|
|
61
|
+
scheduler.after(1000, fn2);
|
|
62
|
+
|
|
63
|
+
scheduler.clear();
|
|
64
|
+
vi.advanceTimersByTime(2000);
|
|
65
|
+
|
|
66
|
+
expect(fn1).not.toHaveBeenCalled();
|
|
67
|
+
expect(fn2).not.toHaveBeenCalled();
|
|
68
|
+
expect(scheduler.size).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should report correct size of active timers', () => {
|
|
72
|
+
scheduler.every(1000, () => { });
|
|
73
|
+
scheduler.after(500, () => { });
|
|
74
|
+
expect(scheduler.size).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw for non-positive interval in every()', () => {
|
|
78
|
+
expect(() => scheduler.every(0, () => { })).toThrow(TypeError);
|
|
79
|
+
expect(() => scheduler.every(-1, () => { })).toThrow(TypeError);
|
|
80
|
+
});
|
|
81
|
+
});
|