actly 1.2.0 → 1.3.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/CHANGELOG.md +234 -0
- package/MIGRATION.md +141 -0
- package/README.md +326 -218
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +406 -220
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +405 -222
- package/dist/core/executor.cjs +0 -27
- package/dist/core/executor.d.ts +0 -41
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +0 -28
- package/dist/core/health.cjs +146 -17
- package/dist/core/health.d.ts +20 -5
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +140 -18
- package/dist/core/shutdown.cjs +19 -8
- package/dist/core/shutdown.d.ts +1 -4
- package/dist/core/shutdown.d.ts.map +1 -1
- package/dist/core/shutdown.js +18 -9
- package/dist/core/tenant.cjs +61 -19
- package/dist/core/tenant.d.ts +5 -10
- package/dist/core/tenant.d.ts.map +1 -1
- package/dist/core/tenant.js +61 -20
- package/dist/errors.cjs +70 -103
- package/dist/errors.d.ts +33 -90
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +66 -103
- package/dist/index.cjs +25 -22
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/observability.cjs +0 -39
- package/dist/observability.d.ts +19 -78
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +0 -40
- package/dist/policies/bulkhead.cjs +44 -4
- package/dist/policies/bulkhead.d.ts +0 -0
- package/dist/policies/bulkhead.d.ts.map +1 -1
- package/dist/policies/bulkhead.js +44 -5
- package/dist/policies/cache.cjs +22 -142
- package/dist/policies/cache.d.ts +0 -46
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +22 -143
- package/dist/policies/circuitBreaker.cjs +125 -31
- package/dist/policies/circuitBreaker.d.ts +0 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -1
- package/dist/policies/circuitBreaker.js +125 -32
- package/dist/policies/dedupe.cjs +19 -66
- package/dist/policies/dedupe.d.ts +0 -23
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +19 -67
- package/dist/policies/noop.cjs +6 -0
- package/dist/policies/noop.d.ts +3 -0
- package/dist/policies/noop.d.ts.map +1 -0
- package/dist/policies/noop.js +3 -0
- package/dist/policies/rateLimit.cjs +15 -5
- package/dist/policies/rateLimit.d.ts +0 -0
- package/dist/policies/rateLimit.d.ts.map +1 -1
- package/dist/policies/rateLimit.js +15 -6
- package/dist/policies/retry.cjs +100 -65
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +100 -66
- package/dist/policies/timeout.cjs +32 -60
- package/dist/policies/timeout.d.ts +0 -13
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +32 -71
- package/dist/stores/base.cjs +0 -10
- package/dist/stores/base.d.ts +4 -67
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +0 -11
- package/dist/stores/memory.cjs +75 -88
- package/dist/stores/memory.d.ts +7 -74
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +75 -89
- package/dist/testing/index.cjs +56 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +52 -0
- package/dist/types/index.cjs +0 -1
- package/dist/types/index.d.ts +17 -307
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +14 -111
- package/dist/utils/abort.d.ts +3 -74
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +14 -112
- package/dist/utils/abortPool.cjs +1 -2
- package/dist/utils/abortPool.d.ts +0 -0
- package/dist/utils/abortPool.d.ts.map +1 -1
- package/dist/utils/abortPool.js +1 -3
- package/dist/utils/backoff.cjs +3 -21
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +3 -22
- package/dist/utils/decorator.cjs +44 -0
- package/dist/utils/decorator.d.ts +3 -0
- package/dist/utils/decorator.d.ts.map +1 -0
- package/dist/utils/decorator.js +41 -0
- package/dist/utils/key.cjs +13 -45
- package/dist/utils/key.d.ts +0 -27
- package/dist/utils/key.d.ts.map +1 -1
- package/dist/utils/key.js +13 -46
- package/dist/utils/limits.cjs +9 -27
- package/dist/utils/limits.d.ts +9 -27
- package/dist/utils/limits.d.ts.map +1 -1
- package/dist/utils/limits.js +9 -28
- package/dist/utils/safeCall.cjs +28 -0
- package/dist/utils/safeCall.d.ts +2 -0
- package/dist/utils/safeCall.d.ts.map +1 -0
- package/dist/utils/safeCall.js +25 -0
- package/dist/utils/sanitize.cjs +22 -1
- package/dist/utils/sanitize.d.ts +0 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +22 -2
- package/dist/utils/validate.cjs +81 -18
- package/dist/utils/validate.d.ts +6 -19
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +81 -19
- package/package.json +38 -4
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/core/health.js.map +0 -1
- package/dist/core/shutdown.js.map +0 -1
- package/dist/core/tenant.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observability.js.map +0 -1
- package/dist/policies/bulkhead.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/circuitBreaker.js.map +0 -1
- package/dist/policies/dedupe.js.map +0 -1
- package/dist/policies/rateLimit.js.map +0 -1
- package/dist/policies/retry.js.map +0 -1
- package/dist/policies/timeout.js.map +0 -1
- package/dist/state/store.cjs +0 -14
- package/dist/state/store.d.ts +0 -11
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -11
- package/dist/state/store.js.map +0 -1
- package/dist/stores/base.js.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/abort.js.map +0 -1
- package/dist/utils/abortPool.js.map +0 -1
- package/dist/utils/backoff.js.map +0 -1
- package/dist/utils/key.js.map +0 -1
- package/dist/utils/limits.js.map +0 -1
- package/dist/utils/sanitize.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/stores/memory.cjs
CHANGED
|
@@ -6,47 +6,55 @@ const limits_js_1 = require("../utils/limits.js");
|
|
|
6
6
|
function isUnrefable(t) {
|
|
7
7
|
return typeof t.unref === 'function';
|
|
8
8
|
}
|
|
9
|
-
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
10
|
-
/**
|
|
11
|
-
* Reference `SyncStateStore` implementation backed by a `Map` + doubly-linked
|
|
12
|
-
* list for LRU.
|
|
13
|
-
*
|
|
14
|
-
* # Properties
|
|
15
|
-
*
|
|
16
|
-
* - `size()` is O(1) — tracked via a counter instead of full scan.
|
|
17
|
-
* - LRU reordering uses an explicit doubly-linked list, avoiding the
|
|
18
|
-
* `delete + set` Map churn that was 2 Map operations per `get()`.
|
|
19
|
-
* - Default `maxSize` is bounded (`LIMITS.DEFAULT_STORE_MAX_SIZE`) when
|
|
20
|
-
* used as the module-level default — prevents unbounded memory growth
|
|
21
|
-
* in long-running servers.
|
|
22
|
-
*
|
|
23
|
-
* # Expiry
|
|
24
|
-
*
|
|
25
|
-
* Lazy on `get()` / `has()`: expired entries are deleted when touched.
|
|
26
|
-
* Background sweep (optional) reclaims entries that are never re-read.
|
|
27
|
-
*/
|
|
28
9
|
class InMemoryStore {
|
|
29
10
|
_sync = true;
|
|
11
|
+
static finalizer;
|
|
12
|
+
static {
|
|
13
|
+
if (typeof FinalizationRegistry === 'function') {
|
|
14
|
+
InMemoryStore.finalizer = new FinalizationRegistry((timer) => {
|
|
15
|
+
try {
|
|
16
|
+
clearInterval(timer);
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
30
22
|
map = new Map();
|
|
31
23
|
maxSize;
|
|
32
|
-
head;
|
|
33
|
-
tail;
|
|
24
|
+
head;
|
|
25
|
+
tail;
|
|
34
26
|
cleanupTimer;
|
|
27
|
+
memoryListener;
|
|
35
28
|
constructor(options = {}) {
|
|
36
|
-
const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize =
|
|
29
|
+
const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize = limits_js_1.LIMITS.DEFAULT_STORE_MAX_SIZE, memoryPressureCleanup = false, } = options;
|
|
37
30
|
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
38
|
-
// Infinity is allowed (unbounded); any other non-positive finite value
|
|
39
|
-
// is a programmer error.
|
|
40
31
|
if (maxSize !== Number.POSITIVE_INFINITY) {
|
|
41
32
|
throw new RangeError(`Actly: InMemoryStore maxSize must be a positive finite number or Infinity, got ${maxSize}`);
|
|
42
33
|
}
|
|
43
34
|
}
|
|
35
|
+
if (maxSize !== Number.POSITIVE_INFINITY && !Number.isInteger(maxSize)) {
|
|
36
|
+
throw new RangeError(`Actly: InMemoryStore maxSize must be a positive integer or Infinity, got ${maxSize}`);
|
|
37
|
+
}
|
|
44
38
|
this.maxSize = maxSize;
|
|
45
39
|
if (autoCleanup) {
|
|
46
|
-
const timer = setInterval(() => this.
|
|
40
|
+
const timer = setInterval(() => this.sweep(), cleanupIntervalMs);
|
|
47
41
|
if (isUnrefable(timer))
|
|
48
42
|
timer.unref();
|
|
49
43
|
this.cleanupTimer = timer;
|
|
44
|
+
InMemoryStore.finalizer?.register(this, timer, this);
|
|
45
|
+
}
|
|
46
|
+
if (memoryPressureCleanup) {
|
|
47
|
+
const processOn = process.on;
|
|
48
|
+
if (typeof processOn === 'function') {
|
|
49
|
+
const memoryListener = () => {
|
|
50
|
+
try {
|
|
51
|
+
this.sweep();
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
};
|
|
55
|
+
processOn.call(process, 'memory', memoryListener);
|
|
56
|
+
this.memoryListener = memoryListener;
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
get(key) {
|
|
@@ -54,55 +62,50 @@ class InMemoryStore {
|
|
|
54
62
|
if (!node)
|
|
55
63
|
return undefined;
|
|
56
64
|
if (node.expiresAt !== null && Date.now() > node.expiresAt) {
|
|
57
|
-
this.
|
|
65
|
+
this.removeNode(node);
|
|
58
66
|
this.map.delete(key);
|
|
59
67
|
return undefined;
|
|
60
68
|
}
|
|
61
|
-
|
|
62
|
-
this._moveToTail(node);
|
|
69
|
+
this.moveToTail(node);
|
|
63
70
|
return node.value;
|
|
64
71
|
}
|
|
65
72
|
set(key, value, ttlMs) {
|
|
66
73
|
const existing = this.map.get(key);
|
|
67
74
|
const now = Date.now();
|
|
68
75
|
if (existing) {
|
|
69
|
-
// Update in place — don't grow size, don't evict.
|
|
70
76
|
existing.value = value;
|
|
71
|
-
existing.expiresAt = ttlMs != null && ttlMs > 0 ? now + ttlMs : null;
|
|
77
|
+
existing.expiresAt = ttlMs != null && Number.isFinite(ttlMs) && ttlMs > 0 ? now + ttlMs : null;
|
|
72
78
|
existing.insertedAt = now;
|
|
73
|
-
this.
|
|
79
|
+
this.moveToTail(existing);
|
|
74
80
|
return;
|
|
75
81
|
}
|
|
76
|
-
// New key — evict if at capacity.
|
|
77
82
|
while (this.map.size >= this.maxSize && this.head) {
|
|
78
83
|
const evict = this.head;
|
|
79
|
-
this.
|
|
84
|
+
this.removeNode(evict);
|
|
80
85
|
this.map.delete(evict.key);
|
|
81
86
|
}
|
|
82
87
|
const node = {
|
|
83
88
|
key,
|
|
84
89
|
value,
|
|
85
|
-
expiresAt: ttlMs != null && ttlMs > 0 ? now + ttlMs : null,
|
|
90
|
+
expiresAt: ttlMs != null && Number.isFinite(ttlMs) && ttlMs > 0 ? now + ttlMs : null,
|
|
86
91
|
insertedAt: now,
|
|
87
92
|
};
|
|
88
93
|
this.map.set(key, node);
|
|
89
|
-
this.
|
|
94
|
+
this.appendTail(node);
|
|
90
95
|
}
|
|
91
96
|
delete(key) {
|
|
92
97
|
const node = this.map.get(key);
|
|
93
98
|
if (!node)
|
|
94
99
|
return;
|
|
95
|
-
this.
|
|
100
|
+
this.removeNode(node);
|
|
96
101
|
this.map.delete(key);
|
|
97
102
|
}
|
|
98
103
|
has(key) {
|
|
99
|
-
// Inline the expiry check to avoid the LRU side-effect of get().
|
|
100
|
-
// `has()` should be a pure query, not a touch.
|
|
101
104
|
const node = this.map.get(key);
|
|
102
105
|
if (!node)
|
|
103
106
|
return false;
|
|
104
107
|
if (node.expiresAt !== null && Date.now() > node.expiresAt) {
|
|
105
|
-
this.
|
|
108
|
+
this.removeNode(node);
|
|
106
109
|
this.map.delete(key);
|
|
107
110
|
return false;
|
|
108
111
|
}
|
|
@@ -113,45 +116,38 @@ class InMemoryStore {
|
|
|
113
116
|
this.head = undefined;
|
|
114
117
|
this.tail = undefined;
|
|
115
118
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Return the count of live (non-expired) entries.
|
|
118
|
-
*
|
|
119
|
-
* O(1) — returns the Map size directly. Expired-but-not-yet-
|
|
120
|
-
* evicted entries are counted; they're reclaimed lazily on next access
|
|
121
|
-
* or by the background sweep. This is intentional: a fully-accurate
|
|
122
|
-
* count would require an O(n) scan, defeating the purpose.
|
|
123
|
-
*
|
|
124
|
-
* Pure query — does NOT touch LRU order.
|
|
125
|
-
*/
|
|
126
119
|
size() {
|
|
127
120
|
return this.map.size;
|
|
128
121
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Stop the background cleanup timer and release internal state.
|
|
131
|
-
* Safe to call multiple times — subsequent calls are no-ops.
|
|
132
|
-
*/
|
|
133
122
|
destroy() {
|
|
134
123
|
if (this.cleanupTimer !== undefined) {
|
|
135
124
|
clearInterval(this.cleanupTimer);
|
|
136
125
|
this.cleanupTimer = undefined;
|
|
126
|
+
InMemoryStore.finalizer?.unregister(this);
|
|
127
|
+
}
|
|
128
|
+
if (this.memoryListener) {
|
|
129
|
+
const processOff = process.off;
|
|
130
|
+
if (typeof processOff === 'function') {
|
|
131
|
+
processOff.call(process, 'memory', this.memoryListener);
|
|
132
|
+
}
|
|
133
|
+
this.memoryListener = undefined;
|
|
137
134
|
}
|
|
135
|
+
this.map.clear();
|
|
136
|
+
this.head = undefined;
|
|
137
|
+
this.tail = undefined;
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
// All operations are O(1). The list runs head (LRU) → tail (MRU).
|
|
142
|
-
_appendTail(node) {
|
|
139
|
+
appendTail(node) {
|
|
143
140
|
if (this.tail) {
|
|
144
141
|
this.tail.next = node;
|
|
145
142
|
node.prev = this.tail;
|
|
146
143
|
node.next = undefined;
|
|
147
144
|
}
|
|
148
145
|
else {
|
|
149
|
-
// Empty list — node is both head and tail.
|
|
150
146
|
this.head = node;
|
|
151
147
|
}
|
|
152
148
|
this.tail = node;
|
|
153
149
|
}
|
|
154
|
-
|
|
150
|
+
removeNode(node) {
|
|
155
151
|
if (node.prev) {
|
|
156
152
|
node.prev.next = node.next;
|
|
157
153
|
}
|
|
@@ -167,43 +163,34 @@ class InMemoryStore {
|
|
|
167
163
|
node.prev = undefined;
|
|
168
164
|
node.next = undefined;
|
|
169
165
|
}
|
|
170
|
-
|
|
166
|
+
moveToTail(node) {
|
|
171
167
|
if (this.tail === node)
|
|
172
|
-
return;
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
168
|
+
return;
|
|
169
|
+
this.removeNode(node);
|
|
170
|
+
this.appendTail(node);
|
|
175
171
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const expired = [];
|
|
185
|
-
for (const [key, node] of this.map) {
|
|
186
|
-
if (node.expiresAt !== null && now > node.expiresAt) {
|
|
187
|
-
expired.push(key);
|
|
172
|
+
sweep() {
|
|
173
|
+
try {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const expired = [];
|
|
176
|
+
for (const [key, node] of this.map) {
|
|
177
|
+
if (node.expiresAt !== null && now > node.expiresAt) {
|
|
178
|
+
expired.push(key);
|
|
179
|
+
}
|
|
188
180
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
181
|
+
for (const key of expired) {
|
|
182
|
+
const node = this.map.get(key);
|
|
183
|
+
if (node) {
|
|
184
|
+
this.removeNode(node);
|
|
185
|
+
this.map.delete(key);
|
|
186
|
+
}
|
|
195
187
|
}
|
|
196
188
|
}
|
|
189
|
+
catch {
|
|
190
|
+
}
|
|
197
191
|
}
|
|
198
192
|
}
|
|
199
193
|
exports.InMemoryStore = InMemoryStore;
|
|
200
|
-
/**
|
|
201
|
-
* Factory for the default module-level store.
|
|
202
|
-
*
|
|
203
|
-
* Bounded by `LIMITS.DEFAULT_STORE_MAX_SIZE` with background sweep —
|
|
204
|
-
* prevents unbounded memory growth in long-running servers without
|
|
205
|
-
* requiring callers to opt in.
|
|
206
|
-
*/
|
|
207
194
|
function createDefaultStore() {
|
|
208
195
|
return new InMemoryStore({
|
|
209
196
|
maxSize: limits_js_1.LIMITS.DEFAULT_STORE_MAX_SIZE,
|
package/dist/stores/memory.d.ts
CHANGED
|
@@ -1,98 +1,31 @@
|
|
|
1
1
|
import type { SyncStateStore } from './base.js';
|
|
2
2
|
export interface InMemoryStoreOptions {
|
|
3
|
-
/**
|
|
4
|
-
* Periodically sweep and remove expired entries in the background.
|
|
5
|
-
*
|
|
6
|
-
* Disabled by default for explicit-store users. The DEFAULT module-level
|
|
7
|
-
* store (used when you call `act()` without `withStore()`) enables this
|
|
8
|
-
* automatically — see `core/act.ts`.
|
|
9
|
-
*/
|
|
10
3
|
autoCleanup?: boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Interval between background sweeps in milliseconds.
|
|
13
|
-
* Defaults to 30 000 (30 seconds). Ignored when `autoCleanup` is false.
|
|
14
|
-
*/
|
|
15
4
|
cleanupIntervalMs?: number;
|
|
16
|
-
/**
|
|
17
|
-
* Maximum number of live entries the store will hold.
|
|
18
|
-
*
|
|
19
|
-
* When `set()` would exceed this limit, the least-recently-used entry is
|
|
20
|
-
* evicted before the new one is inserted (LRU semantics). Updates to an
|
|
21
|
-
* existing key do not trigger eviction.
|
|
22
|
-
*
|
|
23
|
-
* Defaults to `Infinity` (unbounded). Set a finite value for long-running
|
|
24
|
-
* caches with high-cardinality keys to bound memory usage.
|
|
25
|
-
*
|
|
26
|
-
* The LRU order is updated on `get()` and `set()` — both move the accessed
|
|
27
|
-
* key to the most-recent position. Implementation uses a doubly-linked
|
|
28
|
-
* list for O(1) reordering (no `delete + set` Map churn).
|
|
29
|
-
*/
|
|
30
5
|
maxSize?: number;
|
|
6
|
+
memoryPressureCleanup?: boolean;
|
|
31
7
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Reference `SyncStateStore` implementation backed by a `Map` + doubly-linked
|
|
34
|
-
* list for LRU.
|
|
35
|
-
*
|
|
36
|
-
* # Properties
|
|
37
|
-
*
|
|
38
|
-
* - `size()` is O(1) — tracked via a counter instead of full scan.
|
|
39
|
-
* - LRU reordering uses an explicit doubly-linked list, avoiding the
|
|
40
|
-
* `delete + set` Map churn that was 2 Map operations per `get()`.
|
|
41
|
-
* - Default `maxSize` is bounded (`LIMITS.DEFAULT_STORE_MAX_SIZE`) when
|
|
42
|
-
* used as the module-level default — prevents unbounded memory growth
|
|
43
|
-
* in long-running servers.
|
|
44
|
-
*
|
|
45
|
-
* # Expiry
|
|
46
|
-
*
|
|
47
|
-
* Lazy on `get()` / `has()`: expired entries are deleted when touched.
|
|
48
|
-
* Background sweep (optional) reclaims entries that are never re-read.
|
|
49
|
-
*/
|
|
50
8
|
export declare class InMemoryStore implements SyncStateStore {
|
|
51
9
|
readonly _sync: true;
|
|
10
|
+
private static finalizer;
|
|
52
11
|
private readonly map;
|
|
53
12
|
private readonly maxSize;
|
|
54
13
|
private head?;
|
|
55
14
|
private tail?;
|
|
56
15
|
private cleanupTimer;
|
|
16
|
+
private memoryListener;
|
|
57
17
|
constructor(options?: InMemoryStoreOptions);
|
|
58
18
|
get<T>(key: string): T | undefined;
|
|
59
19
|
set<T>(key: string, value: T, ttlMs?: number): void;
|
|
60
20
|
delete(key: string): void;
|
|
61
21
|
has(key: string): boolean;
|
|
62
22
|
clear(): void;
|
|
63
|
-
/**
|
|
64
|
-
* Return the count of live (non-expired) entries.
|
|
65
|
-
*
|
|
66
|
-
* O(1) — returns the Map size directly. Expired-but-not-yet-
|
|
67
|
-
* evicted entries are counted; they're reclaimed lazily on next access
|
|
68
|
-
* or by the background sweep. This is intentional: a fully-accurate
|
|
69
|
-
* count would require an O(n) scan, defeating the purpose.
|
|
70
|
-
*
|
|
71
|
-
* Pure query — does NOT touch LRU order.
|
|
72
|
-
*/
|
|
73
23
|
size(): number;
|
|
74
|
-
/**
|
|
75
|
-
* Stop the background cleanup timer and release internal state.
|
|
76
|
-
* Safe to call multiple times — subsequent calls are no-ops.
|
|
77
|
-
*/
|
|
78
24
|
destroy(): void;
|
|
79
|
-
private
|
|
80
|
-
private
|
|
81
|
-
private
|
|
82
|
-
|
|
83
|
-
* Sweep all entries and remove those past their expiry time.
|
|
84
|
-
* Called by the autoCleanup interval; not part of the public contract.
|
|
85
|
-
*
|
|
86
|
-
* Two-pass to avoid mutating the Map during iteration (spec-safe).
|
|
87
|
-
*/
|
|
88
|
-
private _sweep;
|
|
25
|
+
private appendTail;
|
|
26
|
+
private removeNode;
|
|
27
|
+
private moveToTail;
|
|
28
|
+
private sweep;
|
|
89
29
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Factory for the default module-level store.
|
|
92
|
-
*
|
|
93
|
-
* Bounded by `LIMITS.DEFAULT_STORE_MAX_SIZE` with background sweep —
|
|
94
|
-
* prevents unbounded memory growth in long-running servers without
|
|
95
|
-
* requiring callers to opt in.
|
|
96
|
-
*/
|
|
97
30
|
export declare function createDefaultStore(): InMemoryStore;
|
|
98
31
|
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAgC/C,MAAM,WAAW,oBAAoB;IAKnC,WAAW,CAAC,EAAE,OAAO,CAAA;IAGrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAU1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAQhB,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAcD,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,KAAK,EAAG,IAAI,CAAS;IAS9B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAkE;IAS1F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,IAAI,CAAC,CAAS;IACtB,OAAO,CAAC,IAAI,CAAC,CAAS;IACtB,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,cAAc,CAA4C;gBAEtD,OAAO,GAAE,oBAAyB;IAkD9C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAclC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IA+BnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAOzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAYzB,KAAK,IAAI,IAAI;IAYb,IAAI,IAAI,MAAM;IAUd,OAAO,IAAI,IAAI;IAyBf,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,KAAK;CAoBd;AAOD,wBAAgB,kBAAkB,IAAI,aAAa,CAMlD"}
|
package/dist/stores/memory.js
CHANGED
|
@@ -2,47 +2,55 @@ import { LIMITS } from '../utils/limits.js';
|
|
|
2
2
|
function isUnrefable(t) {
|
|
3
3
|
return typeof t.unref === 'function';
|
|
4
4
|
}
|
|
5
|
-
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
6
|
-
/**
|
|
7
|
-
* Reference `SyncStateStore` implementation backed by a `Map` + doubly-linked
|
|
8
|
-
* list for LRU.
|
|
9
|
-
*
|
|
10
|
-
* # Properties
|
|
11
|
-
*
|
|
12
|
-
* - `size()` is O(1) — tracked via a counter instead of full scan.
|
|
13
|
-
* - LRU reordering uses an explicit doubly-linked list, avoiding the
|
|
14
|
-
* `delete + set` Map churn that was 2 Map operations per `get()`.
|
|
15
|
-
* - Default `maxSize` is bounded (`LIMITS.DEFAULT_STORE_MAX_SIZE`) when
|
|
16
|
-
* used as the module-level default — prevents unbounded memory growth
|
|
17
|
-
* in long-running servers.
|
|
18
|
-
*
|
|
19
|
-
* # Expiry
|
|
20
|
-
*
|
|
21
|
-
* Lazy on `get()` / `has()`: expired entries are deleted when touched.
|
|
22
|
-
* Background sweep (optional) reclaims entries that are never re-read.
|
|
23
|
-
*/
|
|
24
5
|
export class InMemoryStore {
|
|
25
6
|
_sync = true;
|
|
7
|
+
static finalizer;
|
|
8
|
+
static {
|
|
9
|
+
if (typeof FinalizationRegistry === 'function') {
|
|
10
|
+
InMemoryStore.finalizer = new FinalizationRegistry((timer) => {
|
|
11
|
+
try {
|
|
12
|
+
clearInterval(timer);
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
26
18
|
map = new Map();
|
|
27
19
|
maxSize;
|
|
28
|
-
head;
|
|
29
|
-
tail;
|
|
20
|
+
head;
|
|
21
|
+
tail;
|
|
30
22
|
cleanupTimer;
|
|
23
|
+
memoryListener;
|
|
31
24
|
constructor(options = {}) {
|
|
32
|
-
const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize =
|
|
25
|
+
const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize = LIMITS.DEFAULT_STORE_MAX_SIZE, memoryPressureCleanup = false, } = options;
|
|
33
26
|
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
34
|
-
// Infinity is allowed (unbounded); any other non-positive finite value
|
|
35
|
-
// is a programmer error.
|
|
36
27
|
if (maxSize !== Number.POSITIVE_INFINITY) {
|
|
37
28
|
throw new RangeError(`Actly: InMemoryStore maxSize must be a positive finite number or Infinity, got ${maxSize}`);
|
|
38
29
|
}
|
|
39
30
|
}
|
|
31
|
+
if (maxSize !== Number.POSITIVE_INFINITY && !Number.isInteger(maxSize)) {
|
|
32
|
+
throw new RangeError(`Actly: InMemoryStore maxSize must be a positive integer or Infinity, got ${maxSize}`);
|
|
33
|
+
}
|
|
40
34
|
this.maxSize = maxSize;
|
|
41
35
|
if (autoCleanup) {
|
|
42
|
-
const timer = setInterval(() => this.
|
|
36
|
+
const timer = setInterval(() => this.sweep(), cleanupIntervalMs);
|
|
43
37
|
if (isUnrefable(timer))
|
|
44
38
|
timer.unref();
|
|
45
39
|
this.cleanupTimer = timer;
|
|
40
|
+
InMemoryStore.finalizer?.register(this, timer, this);
|
|
41
|
+
}
|
|
42
|
+
if (memoryPressureCleanup) {
|
|
43
|
+
const processOn = process.on;
|
|
44
|
+
if (typeof processOn === 'function') {
|
|
45
|
+
const memoryListener = () => {
|
|
46
|
+
try {
|
|
47
|
+
this.sweep();
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
};
|
|
51
|
+
processOn.call(process, 'memory', memoryListener);
|
|
52
|
+
this.memoryListener = memoryListener;
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
get(key) {
|
|
@@ -50,55 +58,50 @@ export class InMemoryStore {
|
|
|
50
58
|
if (!node)
|
|
51
59
|
return undefined;
|
|
52
60
|
if (node.expiresAt !== null && Date.now() > node.expiresAt) {
|
|
53
|
-
this.
|
|
61
|
+
this.removeNode(node);
|
|
54
62
|
this.map.delete(key);
|
|
55
63
|
return undefined;
|
|
56
64
|
}
|
|
57
|
-
|
|
58
|
-
this._moveToTail(node);
|
|
65
|
+
this.moveToTail(node);
|
|
59
66
|
return node.value;
|
|
60
67
|
}
|
|
61
68
|
set(key, value, ttlMs) {
|
|
62
69
|
const existing = this.map.get(key);
|
|
63
70
|
const now = Date.now();
|
|
64
71
|
if (existing) {
|
|
65
|
-
// Update in place — don't grow size, don't evict.
|
|
66
72
|
existing.value = value;
|
|
67
|
-
existing.expiresAt = ttlMs != null && ttlMs > 0 ? now + ttlMs : null;
|
|
73
|
+
existing.expiresAt = ttlMs != null && Number.isFinite(ttlMs) && ttlMs > 0 ? now + ttlMs : null;
|
|
68
74
|
existing.insertedAt = now;
|
|
69
|
-
this.
|
|
75
|
+
this.moveToTail(existing);
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
72
|
-
// New key — evict if at capacity.
|
|
73
78
|
while (this.map.size >= this.maxSize && this.head) {
|
|
74
79
|
const evict = this.head;
|
|
75
|
-
this.
|
|
80
|
+
this.removeNode(evict);
|
|
76
81
|
this.map.delete(evict.key);
|
|
77
82
|
}
|
|
78
83
|
const node = {
|
|
79
84
|
key,
|
|
80
85
|
value,
|
|
81
|
-
expiresAt: ttlMs != null && ttlMs > 0 ? now + ttlMs : null,
|
|
86
|
+
expiresAt: ttlMs != null && Number.isFinite(ttlMs) && ttlMs > 0 ? now + ttlMs : null,
|
|
82
87
|
insertedAt: now,
|
|
83
88
|
};
|
|
84
89
|
this.map.set(key, node);
|
|
85
|
-
this.
|
|
90
|
+
this.appendTail(node);
|
|
86
91
|
}
|
|
87
92
|
delete(key) {
|
|
88
93
|
const node = this.map.get(key);
|
|
89
94
|
if (!node)
|
|
90
95
|
return;
|
|
91
|
-
this.
|
|
96
|
+
this.removeNode(node);
|
|
92
97
|
this.map.delete(key);
|
|
93
98
|
}
|
|
94
99
|
has(key) {
|
|
95
|
-
// Inline the expiry check to avoid the LRU side-effect of get().
|
|
96
|
-
// `has()` should be a pure query, not a touch.
|
|
97
100
|
const node = this.map.get(key);
|
|
98
101
|
if (!node)
|
|
99
102
|
return false;
|
|
100
103
|
if (node.expiresAt !== null && Date.now() > node.expiresAt) {
|
|
101
|
-
this.
|
|
104
|
+
this.removeNode(node);
|
|
102
105
|
this.map.delete(key);
|
|
103
106
|
return false;
|
|
104
107
|
}
|
|
@@ -109,45 +112,38 @@ export class InMemoryStore {
|
|
|
109
112
|
this.head = undefined;
|
|
110
113
|
this.tail = undefined;
|
|
111
114
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Return the count of live (non-expired) entries.
|
|
114
|
-
*
|
|
115
|
-
* O(1) — returns the Map size directly. Expired-but-not-yet-
|
|
116
|
-
* evicted entries are counted; they're reclaimed lazily on next access
|
|
117
|
-
* or by the background sweep. This is intentional: a fully-accurate
|
|
118
|
-
* count would require an O(n) scan, defeating the purpose.
|
|
119
|
-
*
|
|
120
|
-
* Pure query — does NOT touch LRU order.
|
|
121
|
-
*/
|
|
122
115
|
size() {
|
|
123
116
|
return this.map.size;
|
|
124
117
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Stop the background cleanup timer and release internal state.
|
|
127
|
-
* Safe to call multiple times — subsequent calls are no-ops.
|
|
128
|
-
*/
|
|
129
118
|
destroy() {
|
|
130
119
|
if (this.cleanupTimer !== undefined) {
|
|
131
120
|
clearInterval(this.cleanupTimer);
|
|
132
121
|
this.cleanupTimer = undefined;
|
|
122
|
+
InMemoryStore.finalizer?.unregister(this);
|
|
123
|
+
}
|
|
124
|
+
if (this.memoryListener) {
|
|
125
|
+
const processOff = process.off;
|
|
126
|
+
if (typeof processOff === 'function') {
|
|
127
|
+
processOff.call(process, 'memory', this.memoryListener);
|
|
128
|
+
}
|
|
129
|
+
this.memoryListener = undefined;
|
|
133
130
|
}
|
|
131
|
+
this.map.clear();
|
|
132
|
+
this.head = undefined;
|
|
133
|
+
this.tail = undefined;
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
// All operations are O(1). The list runs head (LRU) → tail (MRU).
|
|
138
|
-
_appendTail(node) {
|
|
135
|
+
appendTail(node) {
|
|
139
136
|
if (this.tail) {
|
|
140
137
|
this.tail.next = node;
|
|
141
138
|
node.prev = this.tail;
|
|
142
139
|
node.next = undefined;
|
|
143
140
|
}
|
|
144
141
|
else {
|
|
145
|
-
// Empty list — node is both head and tail.
|
|
146
142
|
this.head = node;
|
|
147
143
|
}
|
|
148
144
|
this.tail = node;
|
|
149
145
|
}
|
|
150
|
-
|
|
146
|
+
removeNode(node) {
|
|
151
147
|
if (node.prev) {
|
|
152
148
|
node.prev.next = node.next;
|
|
153
149
|
}
|
|
@@ -163,42 +159,33 @@ export class InMemoryStore {
|
|
|
163
159
|
node.prev = undefined;
|
|
164
160
|
node.next = undefined;
|
|
165
161
|
}
|
|
166
|
-
|
|
162
|
+
moveToTail(node) {
|
|
167
163
|
if (this.tail === node)
|
|
168
|
-
return;
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
164
|
+
return;
|
|
165
|
+
this.removeNode(node);
|
|
166
|
+
this.appendTail(node);
|
|
171
167
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const expired = [];
|
|
181
|
-
for (const [key, node] of this.map) {
|
|
182
|
-
if (node.expiresAt !== null && now > node.expiresAt) {
|
|
183
|
-
expired.push(key);
|
|
168
|
+
sweep() {
|
|
169
|
+
try {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
const expired = [];
|
|
172
|
+
for (const [key, node] of this.map) {
|
|
173
|
+
if (node.expiresAt !== null && now > node.expiresAt) {
|
|
174
|
+
expired.push(key);
|
|
175
|
+
}
|
|
184
176
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
177
|
+
for (const key of expired) {
|
|
178
|
+
const node = this.map.get(key);
|
|
179
|
+
if (node) {
|
|
180
|
+
this.removeNode(node);
|
|
181
|
+
this.map.delete(key);
|
|
182
|
+
}
|
|
191
183
|
}
|
|
192
184
|
}
|
|
185
|
+
catch {
|
|
186
|
+
}
|
|
193
187
|
}
|
|
194
188
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Factory for the default module-level store.
|
|
197
|
-
*
|
|
198
|
-
* Bounded by `LIMITS.DEFAULT_STORE_MAX_SIZE` with background sweep —
|
|
199
|
-
* prevents unbounded memory growth in long-running servers without
|
|
200
|
-
* requiring callers to opt in.
|
|
201
|
-
*/
|
|
202
189
|
export function createDefaultStore() {
|
|
203
190
|
return new InMemoryStore({
|
|
204
191
|
maxSize: LIMITS.DEFAULT_STORE_MAX_SIZE,
|
|
@@ -206,4 +193,3 @@ export function createDefaultStore() {
|
|
|
206
193
|
cleanupIntervalMs: LIMITS.DEFAULT_STORE_CLEANUP_INTERVAL_MS,
|
|
207
194
|
});
|
|
208
195
|
}
|
|
209
|
-
//# sourceMappingURL=memory.js.map
|