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.
- package/README.md +232 -434
- package/dist/core/act.cjs +353 -35
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +355 -37
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +2 -0
- package/dist/core/executor.d.ts +6 -1
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +2 -0
- package/dist/core/executor.js.map +1 -1
- package/dist/core/health.cjs +34 -0
- package/dist/core/health.d.ts +18 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +28 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/shutdown.cjs +60 -0
- package/dist/core/shutdown.d.ts +8 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +56 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/tenant.cjs +71 -0
- package/dist/core/tenant.d.ts +29 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +68 -0
- package/dist/core/tenant.js.map +1 -0
- package/dist/errors.cjs +182 -0
- package/dist/errors.d.ts +151 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +171 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +44 -4
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/observability.cjs +45 -0
- package/dist/observability.d.ts +145 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +43 -0
- package/dist/observability.js.map +1 -0
- package/dist/policies/bulkhead.cjs +97 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +95 -0
- package/dist/policies/bulkhead.js.map +1 -0
- package/dist/policies/cache.cjs +168 -31
- package/dist/policies/cache.d.ts +17 -4
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +168 -31
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/circuitBreaker.cjs +85 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +83 -0
- package/dist/policies/circuitBreaker.js.map +1 -0
- package/dist/policies/dedupe.cjs +74 -45
- package/dist/policies/dedupe.d.ts +10 -28
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +74 -45
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/rateLimit.cjs +35 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +33 -0
- package/dist/policies/rateLimit.js.map +1 -0
- package/dist/policies/retry.cjs +45 -8
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +45 -8
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +25 -53
- package/dist/policies/timeout.d.ts +2 -33
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +31 -48
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +3 -3
- package/dist/state/store.d.ts +3 -3
- package/dist/state/store.js +3 -3
- package/dist/stores/memory.cjs +126 -53
- package/dist/stores/memory.d.ts +35 -16
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +125 -53
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +176 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +108 -38
- package/dist/utils/abort.d.ts +43 -19
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +108 -38
- package/dist/utils/abort.js.map +1 -1
- package/dist/utils/abortPool.cjs +26 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +22 -0
- package/dist/utils/abortPool.js.map +1 -0
- package/dist/utils/backoff.cjs +12 -1
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +12 -1
- package/dist/utils/backoff.js.map +1 -1
- package/dist/utils/key.cjs +77 -0
- package/dist/utils/key.d.ts +29 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +75 -0
- package/dist/utils/key.js.map +1 -0
- package/dist/utils/limits.cjs +40 -0
- package/dist/utils/limits.d.ts +39 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +38 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/sanitize.cjs +29 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +26 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validate.cjs +90 -16
- package/dist/utils/validate.d.ts +11 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +85 -16
- package/dist/utils/validate.js.map +1 -1
- package/package.json +6 -5
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
import type { PolicyApplier, TimeoutOptions } from '../types/index.js';
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
|
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"}
|
package/dist/policies/timeout.js
CHANGED
|
@@ -1,46 +1,16 @@
|
|
|
1
1
|
import { linkSignal } from '../utils/abort.js';
|
|
2
|
+
import { TimeoutError, TotalTimeoutError } from '../errors.js';
|
|
2
3
|
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|
|
42
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
73
43
|
const controller = new AbortController();
|
|
74
|
-
|
|
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
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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;
|
|
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"}
|
package/dist/state/store.cjs
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryStore = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* @deprecated
|
|
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
|
|
9
|
+
* in a future major release.
|
|
10
10
|
*
|
|
11
|
-
* The implementation
|
|
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; } });
|
package/dist/state/store.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @deprecated
|
|
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
|
|
6
|
+
* in a future major release.
|
|
7
7
|
*
|
|
8
|
-
* The implementation
|
|
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
|
package/dist/state/store.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @deprecated
|
|
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
|
|
6
|
+
* in a future major release.
|
|
7
7
|
*
|
|
8
|
-
* The implementation
|
|
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
|
package/dist/stores/memory.cjs
CHANGED
|
@@ -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
|
-
* #
|
|
14
|
+
* # Properties
|
|
12
15
|
*
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
46
|
-
if (!
|
|
53
|
+
const node = this.map.get(key);
|
|
54
|
+
if (!node)
|
|
47
55
|
return undefined;
|
|
48
|
-
if (
|
|
49
|
-
this.
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
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
|
|
78
|
-
if (!
|
|
101
|
+
const node = this.map.get(key);
|
|
102
|
+
if (!node)
|
|
79
103
|
return false;
|
|
80
|
-
if (
|
|
81
|
-
this.
|
|
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.
|
|
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
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
*
|
|
124
|
+
* Pure query — does NOT touch LRU order.
|
|
97
125
|
*/
|
|
98
126
|
size() {
|
|
99
|
-
|
|
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,
|
|
132
|
-
if (
|
|
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.
|
|
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
|
+
}
|
package/dist/stores/memory.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
* #
|
|
36
|
+
* # Properties
|
|
37
37
|
*
|
|
38
|
-
* `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
*
|
|
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;
|
|
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"}
|