actly 1.1.5 → 1.2.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 (119) hide show
  1. package/README.md +232 -434
  2. package/dist/core/act.cjs +353 -35
  3. package/dist/core/act.d.ts.map +1 -1
  4. package/dist/core/act.js +355 -37
  5. package/dist/core/act.js.map +1 -1
  6. package/dist/core/executor.cjs +2 -0
  7. package/dist/core/executor.d.ts +6 -1
  8. package/dist/core/executor.d.ts.map +1 -1
  9. package/dist/core/executor.js +2 -0
  10. package/dist/core/executor.js.map +1 -1
  11. package/dist/core/health.cjs +34 -0
  12. package/dist/core/health.d.ts +18 -0
  13. package/dist/core/health.d.ts.map +1 -0
  14. package/dist/core/health.js +28 -0
  15. package/dist/core/health.js.map +1 -0
  16. package/dist/core/shutdown.cjs +60 -0
  17. package/dist/core/shutdown.d.ts +8 -0
  18. package/dist/core/shutdown.d.ts.map +1 -0
  19. package/dist/core/shutdown.js +56 -0
  20. package/dist/core/shutdown.js.map +1 -0
  21. package/dist/core/tenant.cjs +71 -0
  22. package/dist/core/tenant.d.ts +29 -0
  23. package/dist/core/tenant.d.ts.map +1 -0
  24. package/dist/core/tenant.js +68 -0
  25. package/dist/core/tenant.js.map +1 -0
  26. package/dist/errors.cjs +182 -0
  27. package/dist/errors.d.ts +151 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +171 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/index.cjs +44 -4
  32. package/dist/index.d.ts +17 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +17 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/observability.cjs +45 -0
  37. package/dist/observability.d.ts +145 -0
  38. package/dist/observability.d.ts.map +1 -0
  39. package/dist/observability.js +43 -0
  40. package/dist/observability.js.map +1 -0
  41. package/dist/policies/bulkhead.cjs +97 -0
  42. package/dist/policies/bulkhead.d.ts +3 -0
  43. package/dist/policies/bulkhead.d.ts.map +1 -0
  44. package/dist/policies/bulkhead.js +95 -0
  45. package/dist/policies/bulkhead.js.map +1 -0
  46. package/dist/policies/cache.cjs +168 -31
  47. package/dist/policies/cache.d.ts +17 -4
  48. package/dist/policies/cache.d.ts.map +1 -1
  49. package/dist/policies/cache.js +168 -31
  50. package/dist/policies/cache.js.map +1 -1
  51. package/dist/policies/circuitBreaker.cjs +85 -0
  52. package/dist/policies/circuitBreaker.d.ts +3 -0
  53. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  54. package/dist/policies/circuitBreaker.js +83 -0
  55. package/dist/policies/circuitBreaker.js.map +1 -0
  56. package/dist/policies/dedupe.cjs +74 -45
  57. package/dist/policies/dedupe.d.ts +10 -28
  58. package/dist/policies/dedupe.d.ts.map +1 -1
  59. package/dist/policies/dedupe.js +74 -45
  60. package/dist/policies/dedupe.js.map +1 -1
  61. package/dist/policies/rateLimit.cjs +35 -0
  62. package/dist/policies/rateLimit.d.ts +3 -0
  63. package/dist/policies/rateLimit.d.ts.map +1 -0
  64. package/dist/policies/rateLimit.js +33 -0
  65. package/dist/policies/rateLimit.js.map +1 -0
  66. package/dist/policies/retry.cjs +45 -8
  67. package/dist/policies/retry.d.ts.map +1 -1
  68. package/dist/policies/retry.js +45 -8
  69. package/dist/policies/retry.js.map +1 -1
  70. package/dist/policies/timeout.cjs +25 -53
  71. package/dist/policies/timeout.d.ts +2 -33
  72. package/dist/policies/timeout.d.ts.map +1 -1
  73. package/dist/policies/timeout.js +31 -48
  74. package/dist/policies/timeout.js.map +1 -1
  75. package/dist/state/store.cjs +3 -3
  76. package/dist/state/store.d.ts +3 -3
  77. package/dist/state/store.js +3 -3
  78. package/dist/stores/memory.cjs +126 -53
  79. package/dist/stores/memory.d.ts +35 -16
  80. package/dist/stores/memory.d.ts.map +1 -1
  81. package/dist/stores/memory.js +125 -53
  82. package/dist/stores/memory.js.map +1 -1
  83. package/dist/types/index.d.ts +176 -3
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/dist/utils/abort.cjs +108 -38
  86. package/dist/utils/abort.d.ts +43 -19
  87. package/dist/utils/abort.d.ts.map +1 -1
  88. package/dist/utils/abort.js +108 -38
  89. package/dist/utils/abort.js.map +1 -1
  90. package/dist/utils/abortPool.cjs +26 -0
  91. package/dist/utils/abortPool.d.ts +4 -0
  92. package/dist/utils/abortPool.d.ts.map +1 -0
  93. package/dist/utils/abortPool.js +22 -0
  94. package/dist/utils/abortPool.js.map +1 -0
  95. package/dist/utils/backoff.cjs +12 -1
  96. package/dist/utils/backoff.d.ts.map +1 -1
  97. package/dist/utils/backoff.js +12 -1
  98. package/dist/utils/backoff.js.map +1 -1
  99. package/dist/utils/key.cjs +77 -0
  100. package/dist/utils/key.d.ts +29 -0
  101. package/dist/utils/key.d.ts.map +1 -0
  102. package/dist/utils/key.js +75 -0
  103. package/dist/utils/key.js.map +1 -0
  104. package/dist/utils/limits.cjs +40 -0
  105. package/dist/utils/limits.d.ts +39 -0
  106. package/dist/utils/limits.d.ts.map +1 -0
  107. package/dist/utils/limits.js +38 -0
  108. package/dist/utils/limits.js.map +1 -0
  109. package/dist/utils/sanitize.cjs +29 -0
  110. package/dist/utils/sanitize.d.ts +3 -0
  111. package/dist/utils/sanitize.d.ts.map +1 -0
  112. package/dist/utils/sanitize.js +26 -0
  113. package/dist/utils/sanitize.js.map +1 -0
  114. package/dist/utils/validate.cjs +90 -16
  115. package/dist/utils/validate.d.ts +11 -0
  116. package/dist/utils/validate.d.ts.map +1 -1
  117. package/dist/utils/validate.js +85 -16
  118. package/dist/utils/validate.js.map +1 -1
  119. package/package.json +6 -5
@@ -1,37 +1,6 @@
1
1
  import type { PolicyApplier, TimeoutOptions } from '../types/index.js';
2
- /**
3
- * Thrown when a per-attempt `timeout` deadline fires.
4
- *
5
- * Carries the configured `ms` so callers can log/alert precisely:
6
- *
7
- * ```ts
8
- * if (!result.ok && result.error instanceof TimeoutError) {
9
- * console.log(`attempt timed out after ${result.error.ms}ms`)
10
- * }
11
- * ```
12
- */
13
- export declare class TimeoutError extends Error {
14
- readonly ms: number;
15
- constructor(ms: number);
16
- }
17
- /**
18
- * Thrown when the operation-wide `totalTimeout` budget fires.
19
- *
20
- * Distinct from `TimeoutError` (per-attempt) so callers can `instanceof`-check
21
- * which deadline fired:
22
- *
23
- * ```ts
24
- * if (result.error instanceof TotalTimeoutError) {
25
- * // whole operation budget exhausted
26
- * } else if (result.error instanceof TimeoutError) {
27
- * // last attempt's per-attempt deadline fired
28
- * }
29
- * ```
30
- */
31
- export declare class TotalTimeoutError extends Error {
32
- readonly ms: number;
33
- constructor(ms: number);
34
- }
2
+ import { TimeoutError, TotalTimeoutError } from '../errors.js';
3
+ export { TimeoutError, TotalTimeoutError };
35
4
  /**
36
5
  * Per-attempt timeout. Races `fn` against a deadline that resets on retry.
37
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAK5F;;;;;;;;;;GAUG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;gBAEP,EAAE,EAAE,MAAM;CAKvB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;gBAEP,EAAE,EAAE,MAAM;CAKvB;AAgFD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
1
+ {"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAE5F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAa9D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAA;AAyF1C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
@@ -1,46 +1,16 @@
1
1
  import { linkSignal } from '../utils/abort.js';
2
+ import { TimeoutError, TotalTimeoutError } from '../errors.js';
2
3
  // ─── Errors ───────────────────────────────────────────────────────────────────
3
- /**
4
- * Thrown when a per-attempt `timeout` deadline fires.
5
- *
6
- * Carries the configured `ms` so callers can log/alert precisely:
7
- *
8
- * ```ts
9
- * if (!result.ok && result.error instanceof TimeoutError) {
10
- * console.log(`attempt timed out after ${result.error.ms}ms`)
11
- * }
12
- * ```
13
- */
14
- export class TimeoutError extends Error {
15
- ms;
16
- constructor(ms) {
17
- super(`ACT timed out after ${ms}ms`);
18
- this.name = 'TimeoutError';
19
- this.ms = ms;
20
- }
21
- }
22
- /**
23
- * Thrown when the operation-wide `totalTimeout` budget fires.
24
- *
25
- * Distinct from `TimeoutError` (per-attempt) so callers can `instanceof`-check
26
- * which deadline fired:
27
- *
28
- * ```ts
29
- * if (result.error instanceof TotalTimeoutError) {
30
- * // whole operation budget exhausted
31
- * } else if (result.error instanceof TimeoutError) {
32
- * // last attempt's per-attempt deadline fired
33
- * }
34
- * ```
35
- */
36
- export class TotalTimeoutError extends Error {
37
- ms;
38
- constructor(ms) {
39
- super(`ACT total timeout exceeded after ${ms}ms`);
40
- this.name = 'TotalTimeoutError';
41
- this.ms = ms;
42
- }
43
- }
4
+ //
5
+ // TimeoutError and TotalTimeoutError live in `src/errors.ts` and
6
+ // now extend `ActlyError` (which extends `Error`). Existing `instanceof
7
+ // Error` and `instanceof TimeoutError` checks continue to work; new
8
+ // `instanceof ActlyError` and `.code` field give consumers a stable
9
+ // discriminator for telemetry.
10
+ //
11
+ // Re-exported here for backwards compatibility with code that imports
12
+ // from `'actly/policies/timeout'` (the deep path).
13
+ export { TimeoutError, TotalTimeoutError };
44
14
  // ─── Policy ───────────────────────────────────────────────────────────────────
45
15
  /**
46
16
  * Build a timeout policy that throws `ErrorCtor` on deadline.
@@ -69,16 +39,21 @@ export class TotalTimeoutError extends Error {
69
39
  * reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
70
40
  */
71
41
  function makeTimeoutPolicy(opts, ErrorCtor) {
72
- return (fn, _ctx) => async (parentSignal) => {
42
+ return (fn, ctx) => async (parentSignal) => {
73
43
  const controller = new AbortController();
74
- const timerError = new ErrorCtor(opts.ms);
44
+ // Pass key to the error ctor for better debugging context.
45
+ const timerError = new ErrorCtor(opts.ms, { key: ctx.key });
75
46
  // Arm the per-attempt timer. The error object is allocated once so the
76
47
  // stack trace points here (the policy frame), not at setTimeout's
77
48
  // internal callback.
78
49
  const timer = setTimeout(() => controller.abort(timerError), opts.ms);
79
- // Link parent -> child. If parent is already aborted, child aborts
80
- // synchronously with parent's reason.
81
- linkSignal(parentSignal, controller);
50
+ // NOTE: do NOT `unref()` this timer. The timeout IS the operation
51
+ // the caller is awaiting. unref'ing would let Node exit the process
52
+ // while a timeout was pending — silently dropping the operation.
53
+ // The timer is cleared in the finally block below.
54
+ // Link parent → child. Capture the unlink function so we can clean
55
+ // up the listener on success path (contract).
56
+ const unlink = linkSignal(parentSignal, controller);
82
57
  try {
83
58
  // Race fn against the abort event. If fn settles first, we get its
84
59
  // result/error. If the signal aborts first, we reject with reason.
@@ -90,12 +65,20 @@ function makeTimeoutPolicy(opts, ErrorCtor) {
90
65
  reject(controller.signal.reason);
91
66
  return;
92
67
  }
93
- controller.signal.addEventListener('abort', () => reject(controller.signal.reason), { once: true });
94
- Promise.resolve(fn(controller.signal)).then((value) => resolve(value), (error) => reject(error));
68
+ const onAbort = () => reject(controller.signal.reason);
69
+ controller.signal.addEventListener('abort', onAbort, { once: true });
70
+ Promise.resolve(fn(controller.signal)).then((value) => {
71
+ controller.signal.removeEventListener('abort', onAbort);
72
+ resolve(value);
73
+ }, (error) => {
74
+ controller.signal.removeEventListener('abort', onAbort);
75
+ reject(error);
76
+ });
95
77
  });
96
78
  }
97
79
  finally {
98
80
  clearTimeout(timer);
81
+ unlink();
99
82
  }
100
83
  };
101
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,iFAAiF;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,EAAE,CAAQ;IAEnB,YAAY,EAAU;QACpB,KAAK,CAAC,uBAAuB,EAAE,IAAI,CAAC,CAAA;QACpC,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;QAC1B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,EAAE,CAAQ;IAEnB,YAAY,EAAU;QACpB,KAAK,CAAC,oCAAoC,EAAE,IAAI,CAAC,CAAA;QACjD,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;QAC/B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;CACF;AAED,iFAAiF;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,SAAS,iBAAiB,CACxB,IAAoB,EACpB,SAAoC;IAEpC,OAAO,CAAC,EAAY,EAAE,IAAmB,EAAY,EAAE,CACrD,KAAK,EAAE,YAAyB,EAAE,EAAE;QAClC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,UAAU,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAEzC,uEAAuE;QACvE,kEAAkE;QAClE,qBAAqB;QACrB,MAAM,KAAK,GAAG,UAAU,CACtB,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAClC,IAAI,CAAC,EAAE,CACR,CAAA;QAED,mEAAmE;QACnE,sCAAsC;QACtC,UAAU,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;QAEpC,IAAI,CAAC;YACH,mEAAmE;YACnE,mEAAmE;YACnE,EAAE;YACF,oEAAoE;YACpE,8DAA8D;YAC9D,OAAO,MAAM,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC9C,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBAC9B,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;oBAChC,OAAM;gBACR,CAAC;gBAED,UAAU,CAAC,MAAM,CAAC,gBAAgB,CAChC,OAAO,EACP,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EACtC,EAAE,IAAI,EAAE,IAAI,EAAE,CACf,CAAA;gBAED,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CACzC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EACzB,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CACzB,CAAA;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;IACH,CAAC,CAAA;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAI,IAAoB;IACnD,OAAO,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAI,IAAoB;IACxD,OAAO,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;AACnD,CAAC"}
1
+ {"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAE9D,iFAAiF;AACjF,EAAE;AACF,iEAAiE;AACjE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AAC/B,EAAE;AACF,sEAAsE;AACtE,mDAAmD;AAEnD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAA;AAE1C,iFAAiF;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,SAAS,iBAAiB,CACxB,IAAoB,EACpB,SAAgE;IAEhE,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,EAAE,YAAyB,EAAE,EAAE;QAClC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,2DAA2D;QAC3D,MAAM,UAAU,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAA;QAE3D,uEAAuE;QACvE,kEAAkE;QAClE,qBAAqB;QACrB,MAAM,KAAK,GAAG,UAAU,CACtB,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAClC,IAAI,CAAC,EAAE,CACR,CAAA;QACD,kEAAkE;QAClE,oEAAoE;QACpE,iEAAiE;QACjE,mDAAmD;QAEnD,mEAAmE;QACnE,8CAA8C;QAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;QAEnD,IAAI,CAAC;YACH,mEAAmE;YACnE,mEAAmE;YACnE,EAAE;YACF,oEAAoE;YACpE,8DAA8D;YAC9D,OAAO,MAAM,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC9C,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBAC9B,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;oBAChC,OAAM;gBACR,CAAC;gBAED,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;gBACtD,UAAU,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;gBAEpE,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CACzC,CAAC,KAAK,EAAE,EAAE;oBACR,UAAU,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;oBACvD,OAAO,CAAC,KAAK,CAAC,CAAA;gBAChB,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;oBACR,UAAU,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;oBACvD,MAAM,CAAC,KAAK,CAAC,CAAA;gBACf,CAAC,CACF,CAAA;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,EAAE,CAAA;QACV,CAAC;IACH,CAAC,CAAA;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAI,IAAoB;IACnD,OAAO,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAI,IAAoB;IACxD,OAAO,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;AACnD,CAAC"}
@@ -2,13 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryStore = void 0;
4
4
  /**
5
- * @deprecated since v1.1.5 — import from `'actly'` directly.
5
+ * @deprecated import from `'actly'` directly.
6
6
  *
7
7
  * This re-export shim exists for backwards compatibility with code that
8
8
  * imported `InMemoryStore` from `'actly/state/store'`. It will be removed
9
- * in v2.0.0.
9
+ * in a future major release.
10
10
  *
11
- * The implementation has moved to `src/stores/memory.ts`.
11
+ * The implementation lives in `src/stores/memory.ts`.
12
12
  */
13
13
  var memory_js_1 = require("../stores/memory.js");
14
14
  Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return memory_js_1.InMemoryStore; } });
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @deprecated since v1.1.5 — import from `'actly'` directly.
2
+ * @deprecated import from `'actly'` directly.
3
3
  *
4
4
  * This re-export shim exists for backwards compatibility with code that
5
5
  * imported `InMemoryStore` from `'actly/state/store'`. It will be removed
6
- * in v2.0.0.
6
+ * in a future major release.
7
7
  *
8
- * The implementation has moved to `src/stores/memory.ts`.
8
+ * The implementation lives in `src/stores/memory.ts`.
9
9
  */
10
10
  export { InMemoryStore } from '../stores/memory.js';
11
11
  //# sourceMappingURL=store.d.ts.map
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @deprecated since v1.1.5 — import from `'actly'` directly.
2
+ * @deprecated import from `'actly'` directly.
3
3
  *
4
4
  * This re-export shim exists for backwards compatibility with code that
5
5
  * imported `InMemoryStore` from `'actly/state/store'`. It will be removed
6
- * in v2.0.0.
6
+ * in a future major release.
7
7
  *
8
- * The implementation has moved to `src/stores/memory.ts`.
8
+ * The implementation lives in `src/stores/memory.ts`.
9
9
  */
10
10
  export { InMemoryStore } from '../stores/memory.js';
11
11
  //# sourceMappingURL=store.js.map
@@ -1,18 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryStore = void 0;
4
+ exports.createDefaultStore = createDefaultStore;
5
+ const limits_js_1 = require("../utils/limits.js");
4
6
  function isUnrefable(t) {
5
7
  return typeof t.unref === 'function';
6
8
  }
7
9
  // ─── Implementation ───────────────────────────────────────────────────────────
8
10
  /**
9
- * Reference `SyncStateStore` implementation backed by a `Map`.
11
+ * Reference `SyncStateStore` implementation backed by a `Map` + doubly-linked
12
+ * list for LRU.
10
13
  *
11
- * # LRU semantics
14
+ * # Properties
12
15
  *
13
- * `Map` iteration order is insertion order, so we implement LRU by
14
- * `delete` + `set` on every access — the most-recently-touched key ends up
15
- * at the end of the iteration, and the oldest is `entries.keys().next().value`.
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.
16
22
  *
17
23
  * # Expiry
18
24
  *
@@ -21,8 +27,10 @@ function isUnrefable(t) {
21
27
  */
22
28
  class InMemoryStore {
23
29
  _sync = true;
24
- entries = new Map();
30
+ map = new Map();
25
31
  maxSize;
32
+ head; // least recently used
33
+ tail; // most recently used
26
34
  cleanupTimer;
27
35
  constructor(options = {}) {
28
36
  const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize = Number.POSITIVE_INFINITY, } = options;
@@ -42,74 +50,81 @@ class InMemoryStore {
42
50
  }
43
51
  }
44
52
  get(key) {
45
- const entry = this.entries.get(key);
46
- if (!entry)
53
+ const node = this.map.get(key);
54
+ if (!node)
47
55
  return undefined;
48
- if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
49
- this.entries.delete(key);
56
+ if (node.expiresAt !== null && Date.now() > node.expiresAt) {
57
+ this._removeNode(node);
58
+ this.map.delete(key);
50
59
  return undefined;
51
60
  }
52
- // LRU refresh: move to most-recent position.
53
- // delete + set is the canonical pattern for reordering a Map.
54
- this.entries.delete(key);
55
- this.entries.set(key, entry);
56
- return entry.value;
61
+ // LRU refresh: move to tail (most-recent).
62
+ this._moveToTail(node);
63
+ return node.value;
57
64
  }
58
65
  set(key, value, ttlMs) {
59
- // Evict if at capacity AND adding a new key (updates don't grow size).
60
- if (!this.entries.has(key) && this.entries.size >= this.maxSize) {
61
- const oldest = this.entries.keys().next().value;
62
- if (oldest !== undefined)
63
- this.entries.delete(oldest);
66
+ const existing = this.map.get(key);
67
+ const now = Date.now();
68
+ if (existing) {
69
+ // Update in place — don't grow size, don't evict.
70
+ existing.value = value;
71
+ existing.expiresAt = ttlMs != null && ttlMs > 0 ? now + ttlMs : null;
72
+ existing.insertedAt = now;
73
+ this._moveToTail(existing);
74
+ return;
75
+ }
76
+ // New key — evict if at capacity.
77
+ while (this.map.size >= this.maxSize && this.head) {
78
+ const evict = this.head;
79
+ this._removeNode(evict);
80
+ this.map.delete(evict.key);
64
81
  }
65
- const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
66
- // delete + set ensures the key is moved to the most-recent position
67
- // even on update, keeping LRU order consistent.
68
- this.entries.delete(key);
69
- this.entries.set(key, { value, expiresAt });
82
+ const node = {
83
+ key,
84
+ value,
85
+ expiresAt: ttlMs != null && ttlMs > 0 ? now + ttlMs : null,
86
+ insertedAt: now,
87
+ };
88
+ this.map.set(key, node);
89
+ this._appendTail(node);
70
90
  }
71
91
  delete(key) {
72
- this.entries.delete(key);
92
+ const node = this.map.get(key);
93
+ if (!node)
94
+ return;
95
+ this._removeNode(node);
96
+ this.map.delete(key);
73
97
  }
74
98
  has(key) {
75
99
  // Inline the expiry check to avoid the LRU side-effect of get().
76
100
  // `has()` should be a pure query, not a touch.
77
- const entry = this.entries.get(key);
78
- if (!entry)
101
+ const node = this.map.get(key);
102
+ if (!node)
79
103
  return false;
80
- if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
81
- this.entries.delete(key);
104
+ if (node.expiresAt !== null && Date.now() > node.expiresAt) {
105
+ this._removeNode(node);
106
+ this.map.delete(key);
82
107
  return false;
83
108
  }
84
109
  return true;
85
110
  }
86
111
  clear() {
87
- this.entries.clear();
112
+ this.map.clear();
113
+ this.head = undefined;
114
+ this.tail = undefined;
88
115
  }
89
116
  /**
90
117
  * Return the count of live (non-expired) entries.
91
118
  *
92
- * Pure query does NOT touch LRU order. Expired entries discovered during
93
- * the scan are evicted opportunistically (they were already invisible to
94
- * `get()`, so eviction has no observable effect beyond memory reclamation).
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.
95
123
  *
96
- * Two-pass to avoid mutating the Map during iteration (spec-safe).
124
+ * Pure query does NOT touch LRU order.
97
125
  */
98
126
  size() {
99
- const now = Date.now();
100
- const expired = [];
101
- let count = 0;
102
- for (const [key, entry] of this.entries) {
103
- if (entry.expiresAt !== null && entry.expiresAt <= now) {
104
- expired.push(key);
105
- }
106
- else {
107
- count++;
108
- }
109
- }
110
- for (const key of expired)
111
- this.entries.delete(key);
112
- return count;
127
+ return this.map.size;
113
128
  }
114
129
  /**
115
130
  * Stop the background cleanup timer and release internal state.
@@ -121,20 +136,78 @@ class InMemoryStore {
121
136
  this.cleanupTimer = undefined;
122
137
  }
123
138
  }
139
+ // ─── LRU list operations ──────────────────────────────────────────────────
140
+ //
141
+ // All operations are O(1). The list runs head (LRU) → tail (MRU).
142
+ _appendTail(node) {
143
+ if (this.tail) {
144
+ this.tail.next = node;
145
+ node.prev = this.tail;
146
+ node.next = undefined;
147
+ }
148
+ else {
149
+ // Empty list — node is both head and tail.
150
+ this.head = node;
151
+ }
152
+ this.tail = node;
153
+ }
154
+ _removeNode(node) {
155
+ if (node.prev) {
156
+ node.prev.next = node.next;
157
+ }
158
+ else {
159
+ this.head = node.next;
160
+ }
161
+ if (node.next) {
162
+ node.next.prev = node.prev;
163
+ }
164
+ else {
165
+ this.tail = node.prev;
166
+ }
167
+ node.prev = undefined;
168
+ node.next = undefined;
169
+ }
170
+ _moveToTail(node) {
171
+ if (this.tail === node)
172
+ return; // already MRU
173
+ this._removeNode(node);
174
+ this._appendTail(node);
175
+ }
124
176
  /**
125
177
  * Sweep all entries and remove those past their expiry time.
126
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).
127
181
  */
128
182
  _sweep() {
129
183
  const now = Date.now();
130
184
  const expired = [];
131
- for (const [key, entry] of this.entries) {
132
- if (entry.expiresAt !== null && now > entry.expiresAt) {
185
+ for (const [key, node] of this.map) {
186
+ if (node.expiresAt !== null && now > node.expiresAt) {
133
187
  expired.push(key);
134
188
  }
135
189
  }
136
- for (const key of expired)
137
- this.entries.delete(key);
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);
195
+ }
196
+ }
138
197
  }
139
198
  }
140
199
  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
+ function createDefaultStore() {
208
+ return new InMemoryStore({
209
+ maxSize: limits_js_1.LIMITS.DEFAULT_STORE_MAX_SIZE,
210
+ autoCleanup: true,
211
+ cleanupIntervalMs: limits_js_1.LIMITS.DEFAULT_STORE_CLEANUP_INTERVAL_MS,
212
+ });
213
+ }
@@ -3,11 +3,9 @@ export interface InMemoryStoreOptions {
3
3
  /**
4
4
  * Periodically sweep and remove expired entries in the background.
5
5
  *
6
- * Disabled by default. The store evicts lazily on `get()` / `has()` access,
7
- * which is sufficient for most use cases. Enable `autoCleanup` when the
8
- * store is long-lived and accumulates many TTL'd entries that are never
9
- * re-read — for example, a server-side cache that receives write-heavy
10
- * traffic with low subsequent read rates.
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`.
11
9
  */
12
10
  autoCleanup?: boolean;
13
11
  /**
@@ -26,18 +24,23 @@ export interface InMemoryStoreOptions {
26
24
  * caches with high-cardinality keys to bound memory usage.
27
25
  *
28
26
  * The LRU order is updated on `get()` and `set()` — both move the accessed
29
- * key to the most-recent position.
27
+ * key to the most-recent position. Implementation uses a doubly-linked
28
+ * list for O(1) reordering (no `delete + set` Map churn).
30
29
  */
31
30
  maxSize?: number;
32
31
  }
33
32
  /**
34
- * Reference `SyncStateStore` implementation backed by a `Map`.
33
+ * Reference `SyncStateStore` implementation backed by a `Map` + doubly-linked
34
+ * list for LRU.
35
35
  *
36
- * # LRU semantics
36
+ * # Properties
37
37
  *
38
- * `Map` iteration order is insertion order, so we implement LRU by
39
- * `delete` + `set` on every access — the most-recently-touched key ends up
40
- * at the end of the iteration, and the oldest is `entries.keys().next().value`.
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.
41
44
  *
42
45
  * # Expiry
43
46
  *
@@ -46,8 +49,10 @@ export interface InMemoryStoreOptions {
46
49
  */
47
50
  export declare class InMemoryStore implements SyncStateStore {
48
51
  readonly _sync: true;
49
- private readonly entries;
52
+ private readonly map;
50
53
  private readonly maxSize;
54
+ private head?;
55
+ private tail?;
51
56
  private cleanupTimer;
52
57
  constructor(options?: InMemoryStoreOptions);
53
58
  get<T>(key: string): T | undefined;
@@ -58,11 +63,12 @@ export declare class InMemoryStore implements SyncStateStore {
58
63
  /**
59
64
  * Return the count of live (non-expired) entries.
60
65
  *
61
- * Pure query does NOT touch LRU order. Expired entries discovered during
62
- * the scan are evicted opportunistically (they were already invisible to
63
- * `get()`, so eviction has no observable effect beyond memory reclamation).
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.
64
70
  *
65
- * Two-pass to avoid mutating the Map during iteration (spec-safe).
71
+ * Pure query does NOT touch LRU order.
66
72
  */
67
73
  size(): number;
68
74
  /**
@@ -70,10 +76,23 @@ export declare class InMemoryStore implements SyncStateStore {
70
76
  * Safe to call multiple times — subsequent calls are no-ops.
71
77
  */
72
78
  destroy(): void;
79
+ private _appendTail;
80
+ private _removeNode;
81
+ private _moveToTail;
73
82
  /**
74
83
  * Sweep all entries and remove those past their expiry time.
75
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).
76
87
  */
77
88
  private _sweep;
78
89
  }
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
+ export declare function createDefaultStore(): InMemoryStore;
79
98
  //# 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;AAwB/C,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,KAAK,EAAG,IAAI,CAAS;IAE9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,YAAY,CAA4C;gBAEpD,OAAO,GAAE,oBAAyB;IA0B9C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAiBlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAcnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAYzB,KAAK,IAAI,IAAI;IAIb;;;;;;;;OAQG;IACH,IAAI,IAAI,MAAM;IAiBd;;;OAGG;IACH,OAAO,IAAI,IAAI;IAOf;;;OAGG;IACH,OAAO,CAAC,MAAM;CAUf"}
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"}