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.
Files changed (151) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/MIGRATION.md +141 -0
  3. package/README.md +326 -218
  4. package/SECURITY.md +56 -0
  5. package/dist/core/act.cjs +406 -220
  6. package/dist/core/act.d.ts +6 -84
  7. package/dist/core/act.d.ts.map +1 -1
  8. package/dist/core/act.js +405 -222
  9. package/dist/core/executor.cjs +0 -27
  10. package/dist/core/executor.d.ts +0 -41
  11. package/dist/core/executor.d.ts.map +1 -1
  12. package/dist/core/executor.js +0 -28
  13. package/dist/core/health.cjs +146 -17
  14. package/dist/core/health.d.ts +20 -5
  15. package/dist/core/health.d.ts.map +1 -1
  16. package/dist/core/health.js +140 -18
  17. package/dist/core/shutdown.cjs +19 -8
  18. package/dist/core/shutdown.d.ts +1 -4
  19. package/dist/core/shutdown.d.ts.map +1 -1
  20. package/dist/core/shutdown.js +18 -9
  21. package/dist/core/tenant.cjs +61 -19
  22. package/dist/core/tenant.d.ts +5 -10
  23. package/dist/core/tenant.d.ts.map +1 -1
  24. package/dist/core/tenant.js +61 -20
  25. package/dist/errors.cjs +70 -103
  26. package/dist/errors.d.ts +33 -90
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +66 -103
  29. package/dist/index.cjs +25 -22
  30. package/dist/index.d.ts +12 -8
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +7 -15
  33. package/dist/observability.cjs +0 -39
  34. package/dist/observability.d.ts +19 -78
  35. package/dist/observability.d.ts.map +1 -1
  36. package/dist/observability.js +0 -40
  37. package/dist/policies/bulkhead.cjs +44 -4
  38. package/dist/policies/bulkhead.d.ts +0 -0
  39. package/dist/policies/bulkhead.d.ts.map +1 -1
  40. package/dist/policies/bulkhead.js +44 -5
  41. package/dist/policies/cache.cjs +22 -142
  42. package/dist/policies/cache.d.ts +0 -46
  43. package/dist/policies/cache.d.ts.map +1 -1
  44. package/dist/policies/cache.js +22 -143
  45. package/dist/policies/circuitBreaker.cjs +125 -31
  46. package/dist/policies/circuitBreaker.d.ts +0 -0
  47. package/dist/policies/circuitBreaker.d.ts.map +1 -1
  48. package/dist/policies/circuitBreaker.js +125 -32
  49. package/dist/policies/dedupe.cjs +19 -66
  50. package/dist/policies/dedupe.d.ts +0 -23
  51. package/dist/policies/dedupe.d.ts.map +1 -1
  52. package/dist/policies/dedupe.js +19 -67
  53. package/dist/policies/noop.cjs +6 -0
  54. package/dist/policies/noop.d.ts +3 -0
  55. package/dist/policies/noop.d.ts.map +1 -0
  56. package/dist/policies/noop.js +3 -0
  57. package/dist/policies/rateLimit.cjs +15 -5
  58. package/dist/policies/rateLimit.d.ts +0 -0
  59. package/dist/policies/rateLimit.d.ts.map +1 -1
  60. package/dist/policies/rateLimit.js +15 -6
  61. package/dist/policies/retry.cjs +100 -65
  62. package/dist/policies/retry.d.ts +0 -27
  63. package/dist/policies/retry.d.ts.map +1 -1
  64. package/dist/policies/retry.js +100 -66
  65. package/dist/policies/timeout.cjs +32 -60
  66. package/dist/policies/timeout.d.ts +0 -13
  67. package/dist/policies/timeout.d.ts.map +1 -1
  68. package/dist/policies/timeout.js +32 -71
  69. package/dist/stores/base.cjs +0 -10
  70. package/dist/stores/base.d.ts +4 -67
  71. package/dist/stores/base.d.ts.map +1 -1
  72. package/dist/stores/base.js +0 -11
  73. package/dist/stores/memory.cjs +75 -88
  74. package/dist/stores/memory.d.ts +7 -74
  75. package/dist/stores/memory.d.ts.map +1 -1
  76. package/dist/stores/memory.js +75 -89
  77. package/dist/testing/index.cjs +56 -0
  78. package/dist/testing/index.d.ts +6 -0
  79. package/dist/testing/index.d.ts.map +1 -0
  80. package/dist/testing/index.js +52 -0
  81. package/dist/types/index.cjs +0 -1
  82. package/dist/types/index.d.ts +17 -307
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +0 -2
  85. package/dist/utils/abort.cjs +14 -111
  86. package/dist/utils/abort.d.ts +3 -74
  87. package/dist/utils/abort.d.ts.map +1 -1
  88. package/dist/utils/abort.js +14 -112
  89. package/dist/utils/abortPool.cjs +1 -2
  90. package/dist/utils/abortPool.d.ts +0 -0
  91. package/dist/utils/abortPool.d.ts.map +1 -1
  92. package/dist/utils/abortPool.js +1 -3
  93. package/dist/utils/backoff.cjs +3 -21
  94. package/dist/utils/backoff.d.ts +0 -11
  95. package/dist/utils/backoff.d.ts.map +1 -1
  96. package/dist/utils/backoff.js +3 -22
  97. package/dist/utils/decorator.cjs +44 -0
  98. package/dist/utils/decorator.d.ts +3 -0
  99. package/dist/utils/decorator.d.ts.map +1 -0
  100. package/dist/utils/decorator.js +41 -0
  101. package/dist/utils/key.cjs +13 -45
  102. package/dist/utils/key.d.ts +0 -27
  103. package/dist/utils/key.d.ts.map +1 -1
  104. package/dist/utils/key.js +13 -46
  105. package/dist/utils/limits.cjs +9 -27
  106. package/dist/utils/limits.d.ts +9 -27
  107. package/dist/utils/limits.d.ts.map +1 -1
  108. package/dist/utils/limits.js +9 -28
  109. package/dist/utils/safeCall.cjs +28 -0
  110. package/dist/utils/safeCall.d.ts +2 -0
  111. package/dist/utils/safeCall.d.ts.map +1 -0
  112. package/dist/utils/safeCall.js +25 -0
  113. package/dist/utils/sanitize.cjs +22 -1
  114. package/dist/utils/sanitize.d.ts +0 -0
  115. package/dist/utils/sanitize.d.ts.map +1 -1
  116. package/dist/utils/sanitize.js +22 -2
  117. package/dist/utils/validate.cjs +81 -18
  118. package/dist/utils/validate.d.ts +6 -19
  119. package/dist/utils/validate.d.ts.map +1 -1
  120. package/dist/utils/validate.js +81 -19
  121. package/package.json +38 -4
  122. package/dist/core/act.js.map +0 -1
  123. package/dist/core/executor.js.map +0 -1
  124. package/dist/core/health.js.map +0 -1
  125. package/dist/core/shutdown.js.map +0 -1
  126. package/dist/core/tenant.js.map +0 -1
  127. package/dist/errors.js.map +0 -1
  128. package/dist/index.js.map +0 -1
  129. package/dist/observability.js.map +0 -1
  130. package/dist/policies/bulkhead.js.map +0 -1
  131. package/dist/policies/cache.js.map +0 -1
  132. package/dist/policies/circuitBreaker.js.map +0 -1
  133. package/dist/policies/dedupe.js.map +0 -1
  134. package/dist/policies/rateLimit.js.map +0 -1
  135. package/dist/policies/retry.js.map +0 -1
  136. package/dist/policies/timeout.js.map +0 -1
  137. package/dist/state/store.cjs +0 -14
  138. package/dist/state/store.d.ts +0 -11
  139. package/dist/state/store.d.ts.map +0 -1
  140. package/dist/state/store.js +0 -11
  141. package/dist/state/store.js.map +0 -1
  142. package/dist/stores/base.js.map +0 -1
  143. package/dist/stores/memory.js.map +0 -1
  144. package/dist/types/index.js.map +0 -1
  145. package/dist/utils/abort.js.map +0 -1
  146. package/dist/utils/abortPool.js.map +0 -1
  147. package/dist/utils/backoff.js.map +0 -1
  148. package/dist/utils/key.js.map +0 -1
  149. package/dist/utils/limits.js.map +0 -1
  150. package/dist/utils/sanitize.js.map +0 -1
  151. package/dist/utils/validate.js.map +0 -1
@@ -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; // least recently used
33
- tail; // most recently used
24
+ head;
25
+ tail;
34
26
  cleanupTimer;
27
+ memoryListener;
35
28
  constructor(options = {}) {
36
- const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize = Number.POSITIVE_INFINITY, } = options;
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._sweep(), cleanupIntervalMs);
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._removeNode(node);
65
+ this.removeNode(node);
58
66
  this.map.delete(key);
59
67
  return undefined;
60
68
  }
61
- // LRU refresh: move to tail (most-recent).
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._moveToTail(existing);
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._removeNode(evict);
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._appendTail(node);
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._removeNode(node);
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._removeNode(node);
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
- // ─── LRU list operations ──────────────────────────────────────────────────
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
- _removeNode(node) {
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
- _moveToTail(node) {
166
+ moveToTail(node) {
171
167
  if (this.tail === node)
172
- return; // already MRU
173
- this._removeNode(node);
174
- this._appendTail(node);
168
+ return;
169
+ this.removeNode(node);
170
+ this.appendTail(node);
175
171
  }
176
- /**
177
- * Sweep all entries and remove those past their expiry time.
178
- * Called by the autoCleanup interval; not part of the public contract.
179
- *
180
- * Two-pass to avoid mutating the Map during iteration (spec-safe).
181
- */
182
- _sweep() {
183
- const now = Date.now();
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
- for (const key of expired) {
191
- const node = this.map.get(key);
192
- if (node) {
193
- this._removeNode(node);
194
- this.map.delete(key);
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,
@@ -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 _appendTail;
80
- private _removeNode;
81
- private _moveToTail;
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;AAkC/C,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,KAAK,EAAG,IAAI,CAAS;IAE9B,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;gBAEpD,OAAO,GAAE,oBAAyB;IA0B9C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAgBlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IA8BnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAOzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAazB,KAAK,IAAI,IAAI;IAMb;;;;;;;;;OASG;IACH,IAAI,IAAI,MAAM;IAId;;;OAGG;IACH,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,WAAW;IAMnB;;;;;OAKG;IACH,OAAO,CAAC,MAAM;CAgBf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,aAAa,CAMlD"}
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"}
@@ -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; // least recently used
29
- tail; // most recently used
20
+ head;
21
+ tail;
30
22
  cleanupTimer;
23
+ memoryListener;
31
24
  constructor(options = {}) {
32
- const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize = Number.POSITIVE_INFINITY, } = options;
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._sweep(), cleanupIntervalMs);
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._removeNode(node);
61
+ this.removeNode(node);
54
62
  this.map.delete(key);
55
63
  return undefined;
56
64
  }
57
- // LRU refresh: move to tail (most-recent).
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._moveToTail(existing);
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._removeNode(evict);
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._appendTail(node);
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._removeNode(node);
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._removeNode(node);
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
- // ─── LRU list operations ──────────────────────────────────────────────────
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
- _removeNode(node) {
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
- _moveToTail(node) {
162
+ moveToTail(node) {
167
163
  if (this.tail === node)
168
- return; // already MRU
169
- this._removeNode(node);
170
- this._appendTail(node);
164
+ return;
165
+ this.removeNode(node);
166
+ this.appendTail(node);
171
167
  }
172
- /**
173
- * Sweep all entries and remove those past their expiry time.
174
- * Called by the autoCleanup interval; not part of the public contract.
175
- *
176
- * Two-pass to avoid mutating the Map during iteration (spec-safe).
177
- */
178
- _sweep() {
179
- const now = Date.now();
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
- for (const key of expired) {
187
- const node = this.map.get(key);
188
- if (node) {
189
- this._removeNode(node);
190
- this.map.delete(key);
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