actly 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -0
- package/MIGRATION.md +141 -0
- package/README.md +334 -428
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +630 -126
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +630 -129
- package/dist/core/executor.cjs +1 -26
- package/dist/core/executor.d.ts +2 -38
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +1 -27
- package/dist/core/health.cjs +163 -0
- package/dist/core/health.d.ts +33 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +150 -0
- package/dist/core/shutdown.cjs +71 -0
- package/dist/core/shutdown.d.ts +5 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +65 -0
- package/dist/core/tenant.cjs +113 -0
- package/dist/core/tenant.d.ts +24 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +109 -0
- package/dist/errors.cjs +149 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +134 -0
- package/dist/index.cjs +51 -8
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/observability.cjs +6 -0
- package/dist/observability.d.ts +86 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +3 -0
- package/dist/policies/bulkhead.cjs +137 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +134 -0
- package/dist/policies/cache.cjs +84 -67
- package/dist/policies/cache.d.ts +0 -33
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +84 -68
- package/dist/policies/circuitBreaker.cjs +179 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +176 -0
- package/dist/policies/dedupe.cjs +46 -64
- package/dist/policies/dedupe.d.ts +0 -41
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +46 -65
- package/dist/policies/noop.cjs +6 -0
- package/dist/policies/noop.d.ts +3 -0
- package/dist/policies/noop.d.ts.map +1 -0
- package/dist/policies/noop.js +3 -0
- package/dist/policies/rateLimit.cjs +45 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +42 -0
- package/dist/policies/retry.cjs +129 -57
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +129 -58
- package/dist/policies/timeout.cjs +47 -103
- package/dist/policies/timeout.d.ts +2 -46
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +46 -102
- package/dist/stores/base.cjs +0 -10
- package/dist/stores/base.d.ts +4 -67
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +0 -11
- package/dist/stores/memory.cjs +147 -87
- package/dist/stores/memory.d.ts +11 -59
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +146 -88
- package/dist/testing/index.cjs +56 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +52 -0
- package/dist/types/index.cjs +0 -1
- package/dist/types/index.d.ts +62 -179
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +46 -73
- package/dist/utils/abort.d.ts +4 -51
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +46 -74
- package/dist/utils/abortPool.cjs +25 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +20 -0
- package/dist/utils/backoff.cjs +8 -15
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +8 -16
- package/dist/utils/decorator.cjs +44 -0
- package/dist/utils/decorator.d.ts +3 -0
- package/dist/utils/decorator.d.ts.map +1 -0
- package/dist/utils/decorator.js +41 -0
- package/dist/utils/key.cjs +45 -0
- package/dist/utils/key.d.ts +2 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +42 -0
- package/dist/utils/limits.cjs +22 -0
- package/dist/utils/limits.d.ts +21 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +19 -0
- package/dist/utils/safeCall.cjs +28 -0
- package/dist/utils/safeCall.d.ts +2 -0
- package/dist/utils/safeCall.d.ts.map +1 -0
- package/dist/utils/safeCall.js +25 -0
- package/dist/utils/sanitize.cjs +50 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +46 -0
- package/dist/utils/validate.cjs +163 -26
- package/dist/utils/validate.d.ts +6 -8
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +158 -27
- package/package.json +42 -7
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/dedupe.js.map +0 -1
- package/dist/policies/retry.js.map +0 -1
- package/dist/policies/timeout.js.map +0 -1
- package/dist/state/store.cjs +0 -14
- package/dist/state/store.d.ts +0 -11
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -11
- package/dist/state/store.js.map +0 -1
- package/dist/stores/base.js.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/abort.js.map +0 -1
- package/dist/utils/backoff.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
|
@@ -1,50 +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
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
37
|
-
*
|
|
38
|
-
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
39
|
-
* own clock.
|
|
40
|
-
*/
|
|
2
|
+
import { TimeoutError, TotalTimeoutError } from '../errors.js';
|
|
3
|
+
export { TimeoutError, TotalTimeoutError };
|
|
41
4
|
export declare function timeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
|
|
42
|
-
/**
|
|
43
|
-
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
44
|
-
* delays) against a hard budget that does NOT reset.
|
|
45
|
-
*
|
|
46
|
-
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
47
|
-
* policy runs and stops regardless of what the inner chain is doing.
|
|
48
|
-
*/
|
|
49
5
|
export declare function totalTimeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
|
|
50
6
|
//# sourceMappingURL=timeout.d.ts.map
|
|
@@ -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;AAG5F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAI9D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAA;AAyG1C,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAMD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
|
package/dist/policies/timeout.js
CHANGED
|
@@ -1,121 +1,65 @@
|
|
|
1
1
|
import { linkSignal } from '../utils/abort.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
}
|
|
44
|
-
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
45
|
-
/**
|
|
46
|
-
* Build a timeout policy that throws `ErrorCtor` on deadline.
|
|
47
|
-
*
|
|
48
|
-
* # Cancellation contract
|
|
49
|
-
*
|
|
50
|
-
* Each invocation:
|
|
51
|
-
* 1. Creates a fresh `AbortController` for this attempt.
|
|
52
|
-
* 2. Arms a `setTimeout` that aborts the controller with a fresh `ErrorCtor(ms)`.
|
|
53
|
-
* 3. Links the parent signal: if the parent aborts (e.g. `totalTimeout` or
|
|
54
|
-
* caller cancellation), the child aborts with the parent's reason.
|
|
55
|
-
* 4. Races `fn(childSignal)` against the abort event.
|
|
56
|
-
*
|
|
57
|
-
* The race is critical: it ensures `act()` returns promptly even if `fn`
|
|
58
|
-
* ignores the signal. The underlying `fn` may keep running in the background
|
|
59
|
-
* (resource leak), but the caller is unblocked. This is the best JavaScript
|
|
60
|
-
* can do without cooperation from `fn`.
|
|
61
|
-
*
|
|
62
|
-
* If `fn` cooperates (passes `signal` to `fetch`, `AbortController`, etc.),
|
|
63
|
-
* the underlying work is cancelled properly — no leak.
|
|
64
|
-
*
|
|
65
|
-
* # Error attribution
|
|
66
|
-
*
|
|
67
|
-
* If the per-attempt timer fires, we throw `ErrorCtor(ms)` regardless of
|
|
68
|
-
* what `fn` does. If the parent signal fires first, we throw the parent's
|
|
69
|
-
* reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
|
|
70
|
-
*/
|
|
71
|
-
function makeTimeoutPolicy(opts, ErrorCtor) {
|
|
72
|
-
return (fn, _ctx) => async (parentSignal) => {
|
|
2
|
+
import { safeCall } from '../utils/safeCall.js';
|
|
3
|
+
import { TimeoutError, TotalTimeoutError } from '../errors.js';
|
|
4
|
+
export { TimeoutError, TotalTimeoutError };
|
|
5
|
+
function makeTimeoutPolicy(opts, errorCtor, kind) {
|
|
6
|
+
const strategy = opts.strategy ?? 'race';
|
|
7
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
73
8
|
const controller = new AbortController();
|
|
74
|
-
const timerError = new
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
9
|
+
const timerError = new errorCtor(opts.ms, { key: ctx.key });
|
|
10
|
+
const obs = ctx.observability;
|
|
11
|
+
let timedOut = false;
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
timedOut = true;
|
|
14
|
+
if (obs) {
|
|
15
|
+
safeCall(obs.hooks.onTimeout, {
|
|
16
|
+
type: 'timeout', key: ctx.key, traceId: obs.traceId,
|
|
17
|
+
timestamp: Date.now(), kind, ms: opts.ms,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
controller.abort(timerError);
|
|
21
|
+
}, opts.ms);
|
|
22
|
+
const unlink = linkSignal(parentSignal, controller);
|
|
82
23
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
24
|
+
if (strategy === 'cooperative') {
|
|
25
|
+
if (parentSignal.aborted) {
|
|
26
|
+
throw parentSignal.reason;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const value = await fn(controller.signal);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (timedOut)
|
|
34
|
+
throw timerError;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
88
38
|
return await new Promise((resolve, reject) => {
|
|
89
39
|
if (controller.signal.aborted) {
|
|
90
40
|
reject(controller.signal.reason);
|
|
91
41
|
return;
|
|
92
42
|
}
|
|
93
|
-
|
|
94
|
-
|
|
43
|
+
const onAbort = () => reject(controller.signal.reason);
|
|
44
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
45
|
+
Promise.resolve(fn(controller.signal)).then((value) => {
|
|
46
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
47
|
+
resolve(value);
|
|
48
|
+
}, (error) => {
|
|
49
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
50
|
+
reject(error);
|
|
51
|
+
});
|
|
95
52
|
});
|
|
96
53
|
}
|
|
97
54
|
finally {
|
|
98
55
|
clearTimeout(timer);
|
|
56
|
+
unlink();
|
|
99
57
|
}
|
|
100
58
|
};
|
|
101
59
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Per-attempt timeout. Races `fn` against a deadline that resets on retry.
|
|
104
|
-
*
|
|
105
|
-
* Place this INSIDE `retryPolicy` (closer to `fn`) so each attempt has its
|
|
106
|
-
* own clock.
|
|
107
|
-
*/
|
|
108
60
|
export function timeoutPolicy(opts) {
|
|
109
|
-
return makeTimeoutPolicy(opts, TimeoutError);
|
|
61
|
+
return makeTimeoutPolicy(opts, TimeoutError, 'per-attempt');
|
|
110
62
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
|
|
113
|
-
* delays) against a hard budget that does NOT reset.
|
|
114
|
-
*
|
|
115
|
-
* Place this as the OUTERMOST policy so the clock starts before any other
|
|
116
|
-
* policy runs and stops regardless of what the inner chain is doing.
|
|
117
|
-
*/
|
|
118
63
|
export function totalTimeoutPolicy(opts) {
|
|
119
|
-
return makeTimeoutPolicy(opts, TotalTimeoutError);
|
|
64
|
+
return makeTimeoutPolicy(opts, TotalTimeoutError, 'total');
|
|
120
65
|
}
|
|
121
|
-
//# sourceMappingURL=timeout.js.map
|
package/dist/stores/base.cjs
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// ─── Store interfaces ─────────────────────────────────────────────────────────
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.isSyncStore = isSyncStore;
|
|
5
4
|
exports.isAsyncStore = isAsyncStore;
|
|
6
|
-
// ─── Type guards ──────────────────────────────────────────────────────────────
|
|
7
|
-
/**
|
|
8
|
-
* Narrows `AnyStateStore` to `SyncStateStore` via the `_sync` discriminant.
|
|
9
|
-
* Used by `execute()` and `cachePolicy` to branch between sync and async paths.
|
|
10
|
-
*/
|
|
11
5
|
function isSyncStore(store) {
|
|
12
6
|
return store._sync === true;
|
|
13
7
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Narrows `AnyStateStore` to `AsyncStateStore` via the `_sync` discriminant.
|
|
16
|
-
* Provided for symmetry; prefer `isSyncStore` for the common guard pattern.
|
|
17
|
-
*/
|
|
18
8
|
function isAsyncStore(store) {
|
|
19
9
|
return store._sync === false;
|
|
20
10
|
}
|
package/dist/stores/base.d.ts
CHANGED
|
@@ -1,88 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Synchronous key-value store. All operations complete in the same tick.
|
|
3
|
-
*
|
|
4
|
-
* # Why synchronous?
|
|
5
|
-
*
|
|
6
|
-
* `dedupePolicy` must read an in-flight Promise from the store and, if absent,
|
|
7
|
-
* write a new one — all within a single synchronous frame. If `get()` were
|
|
8
|
-
* async, two concurrent callers could both observe a miss before either
|
|
9
|
-
* write lands, defeating deduplication entirely. There is no lock primitive
|
|
10
|
-
* in JavaScript that can paper over this: the constraint is structural, not
|
|
11
|
-
* implementation-level.
|
|
12
|
-
*
|
|
13
|
-
* # The `_sync` discriminant
|
|
14
|
-
*
|
|
15
|
-
* `_sync: true` is a runtime tag that lets `execute()` enforce the dedupe
|
|
16
|
-
* constraint for plain-JS callers who bypass TypeScript. It is NOT part of
|
|
17
|
-
* the semantic contract and MUST NOT be used for anything beyond that guard.
|
|
18
|
-
* Implementations must set it as a `readonly` literal (`true as const`).
|
|
19
|
-
*
|
|
20
|
-
* # Lifecycle
|
|
21
|
-
*
|
|
22
|
-
* Implementations that hold background resources (timers, connections) should
|
|
23
|
-
* expose a `destroy()` method as a convention. See `InMemoryStore` for the
|
|
24
|
-
* reference pattern.
|
|
25
|
-
*/
|
|
26
1
|
export interface SyncStateStore {
|
|
27
|
-
/** Runtime discriminant. Must be `true as const`. */
|
|
28
2
|
readonly _sync: true;
|
|
29
3
|
get<T>(key: string): T | undefined;
|
|
30
4
|
set<T>(key: string, value: T, ttlMs?: number): void;
|
|
31
5
|
delete(key: string): void;
|
|
32
6
|
has(key: string): boolean;
|
|
33
|
-
|
|
34
|
-
* Remove all entries synchronously.
|
|
35
|
-
* After `clear()` returns, `size()` returns 0.
|
|
36
|
-
*/
|
|
7
|
+
deleteIfExists?(key: string): boolean;
|
|
37
8
|
clear(): void;
|
|
38
|
-
/**
|
|
39
|
-
* Return the count of live (non-expired) entries.
|
|
40
|
-
* Side-effect free: does not mutate the store. Implementations MAY evict
|
|
41
|
-
* expired entries opportunistically during this call, but MUST NOT have
|
|
42
|
-
* observable side effects beyond internal cleanup.
|
|
43
|
-
*/
|
|
44
9
|
size(): number;
|
|
10
|
+
destroy?(): void;
|
|
45
11
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Asynchronous key-value store. All operations return Promises.
|
|
48
|
-
*
|
|
49
|
-
* # Policy compatibility
|
|
50
|
-
*
|
|
51
|
-
* Compatible with `cachePolicy` only. Passing an `AsyncStateStore` to a
|
|
52
|
-
* policy chain that includes `dedupePolicy` is a TypeScript error and a
|
|
53
|
-
* runtime error — `execute()` throws at chain-build time. See
|
|
54
|
-
* `SyncStateStore` for why dedupe requires synchronous access.
|
|
55
|
-
*
|
|
56
|
-
* # TTL semantics
|
|
57
|
-
*
|
|
58
|
-
* The store is responsible for honouring `ttlMs`. Actly passes it as a hint.
|
|
59
|
-
* Implementations may delegate to a native TTL mechanism (e.g. Redis EXPIRE).
|
|
60
|
-
*
|
|
61
|
-
* # The `_sync` discriminant
|
|
62
|
-
*
|
|
63
|
-
* `_sync: false` mirrors the discriminant on `SyncStateStore`. Must be set
|
|
64
|
-
* as a `readonly` literal (`false as const`).
|
|
65
|
-
*/
|
|
66
12
|
export interface AsyncStateStore {
|
|
67
|
-
/** Runtime discriminant. Must be `false as const`. */
|
|
68
13
|
readonly _sync: false;
|
|
69
14
|
get<T>(key: string): Promise<T | undefined>;
|
|
70
15
|
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
71
16
|
delete(key: string): Promise<void>;
|
|
72
17
|
has(key: string): Promise<boolean>;
|
|
73
|
-
|
|
18
|
+
deleteIfExists?(key: string): Promise<boolean>;
|
|
74
19
|
clear(): Promise<void>;
|
|
75
|
-
/** Return the count of live (non-expired) entries. */
|
|
76
20
|
size(): Promise<number>;
|
|
21
|
+
destroy?(): void | Promise<void>;
|
|
77
22
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Narrows `AnyStateStore` to `SyncStateStore` via the `_sync` discriminant.
|
|
80
|
-
* Used by `execute()` and `cachePolicy` to branch between sync and async paths.
|
|
81
|
-
*/
|
|
82
23
|
export declare function isSyncStore(store: SyncStateStore | AsyncStateStore): store is SyncStateStore;
|
|
83
|
-
/**
|
|
84
|
-
* Narrows `AnyStateStore` to `AsyncStateStore` via the `_sync` discriminant.
|
|
85
|
-
* Provided for symmetry; prefer `isSyncStore` for the common guard pattern.
|
|
86
|
-
*/
|
|
87
24
|
export declare function isAsyncStore(store: SyncStateStore | AsyncStateStore): store is AsyncStateStore;
|
|
88
25
|
//# sourceMappingURL=base.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/stores/base.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/stores/base.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAE7B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;IAEpB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAA;IAClC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IASzB,cAAc,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IAGrC,KAAK,IAAI,IAAI,CAAA;IAMb,IAAI,IAAI,MAAM,CAAA;IAOd,OAAO,CAAC,IAAI,IAAI,CAAA;CACjB;AAcD,MAAM,WAAW,eAAe;IAE9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IAErB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IAC3C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAMlC,cAAc,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAG9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAGtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAMvB,OAAO,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC;AAQD,wBAAgB,WAAW,CACzB,KAAK,EAAE,cAAc,GAAG,eAAe,GACtC,KAAK,IAAI,cAAc,CAEzB;AAMD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,GAAG,eAAe,GACtC,KAAK,IAAI,eAAe,CAE1B"}
|
package/dist/stores/base.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
// ─── Store interfaces ─────────────────────────────────────────────────────────
|
|
2
|
-
// ─── Type guards ──────────────────────────────────────────────────────────────
|
|
3
|
-
/**
|
|
4
|
-
* Narrows `AnyStateStore` to `SyncStateStore` via the `_sync` discriminant.
|
|
5
|
-
* Used by `execute()` and `cachePolicy` to branch between sync and async paths.
|
|
6
|
-
*/
|
|
7
1
|
export function isSyncStore(store) {
|
|
8
2
|
return store._sync === true;
|
|
9
3
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Narrows `AnyStateStore` to `AsyncStateStore` via the `_sync` discriminant.
|
|
12
|
-
* Provided for symmetry; prefer `isSyncStore` for the common guard pattern.
|
|
13
|
-
*/
|
|
14
4
|
export function isAsyncStore(store) {
|
|
15
5
|
return store._sync === false;
|
|
16
6
|
}
|
|
17
|
-
//# sourceMappingURL=base.js.map
|
package/dist/stores/memory.cjs
CHANGED
|
@@ -1,140 +1,200 @@
|
|
|
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
|
-
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
8
|
-
/**
|
|
9
|
-
* Reference `SyncStateStore` implementation backed by a `Map`.
|
|
10
|
-
*
|
|
11
|
-
* # LRU semantics
|
|
12
|
-
*
|
|
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
|
-
*
|
|
17
|
-
* # Expiry
|
|
18
|
-
*
|
|
19
|
-
* Lazy on `get()` / `has()`: expired entries are deleted when touched.
|
|
20
|
-
* Background sweep (optional) reclaims entries that are never re-read.
|
|
21
|
-
*/
|
|
22
9
|
class InMemoryStore {
|
|
23
10
|
_sync = true;
|
|
24
|
-
|
|
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
|
+
}
|
|
22
|
+
map = new Map();
|
|
25
23
|
maxSize;
|
|
24
|
+
head;
|
|
25
|
+
tail;
|
|
26
26
|
cleanupTimer;
|
|
27
|
+
memoryListener;
|
|
27
28
|
constructor(options = {}) {
|
|
28
|
-
const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize =
|
|
29
|
+
const { autoCleanup = false, cleanupIntervalMs = 30_000, maxSize = limits_js_1.LIMITS.DEFAULT_STORE_MAX_SIZE, memoryPressureCleanup = false, } = options;
|
|
29
30
|
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
30
|
-
// Infinity is allowed (unbounded); any other non-positive finite value
|
|
31
|
-
// is a programmer error.
|
|
32
31
|
if (maxSize !== Number.POSITIVE_INFINITY) {
|
|
33
32
|
throw new RangeError(`Actly: InMemoryStore maxSize must be a positive finite number or Infinity, got ${maxSize}`);
|
|
34
33
|
}
|
|
35
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
|
+
}
|
|
36
38
|
this.maxSize = maxSize;
|
|
37
39
|
if (autoCleanup) {
|
|
38
|
-
const timer = setInterval(() => this.
|
|
40
|
+
const timer = setInterval(() => this.sweep(), cleanupIntervalMs);
|
|
39
41
|
if (isUnrefable(timer))
|
|
40
42
|
timer.unref();
|
|
41
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
|
+
}
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
60
|
get(key) {
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
61
|
+
const node = this.map.get(key);
|
|
62
|
+
if (!node)
|
|
47
63
|
return undefined;
|
|
48
|
-
if (
|
|
49
|
-
this.
|
|
64
|
+
if (node.expiresAt !== null && Date.now() > node.expiresAt) {
|
|
65
|
+
this.removeNode(node);
|
|
66
|
+
this.map.delete(key);
|
|
50
67
|
return undefined;
|
|
51
68
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.entries.delete(key);
|
|
55
|
-
this.entries.set(key, entry);
|
|
56
|
-
return entry.value;
|
|
69
|
+
this.moveToTail(node);
|
|
70
|
+
return node.value;
|
|
57
71
|
}
|
|
58
72
|
set(key, value, ttlMs) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.
|
|
69
|
-
|
|
73
|
+
const existing = this.map.get(key);
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (existing) {
|
|
76
|
+
existing.value = value;
|
|
77
|
+
existing.expiresAt = ttlMs != null && Number.isFinite(ttlMs) && ttlMs > 0 ? now + ttlMs : null;
|
|
78
|
+
existing.insertedAt = now;
|
|
79
|
+
this.moveToTail(existing);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
while (this.map.size >= this.maxSize && this.head) {
|
|
83
|
+
const evict = this.head;
|
|
84
|
+
this.removeNode(evict);
|
|
85
|
+
this.map.delete(evict.key);
|
|
86
|
+
}
|
|
87
|
+
const node = {
|
|
88
|
+
key,
|
|
89
|
+
value,
|
|
90
|
+
expiresAt: ttlMs != null && Number.isFinite(ttlMs) && ttlMs > 0 ? now + ttlMs : null,
|
|
91
|
+
insertedAt: now,
|
|
92
|
+
};
|
|
93
|
+
this.map.set(key, node);
|
|
94
|
+
this.appendTail(node);
|
|
70
95
|
}
|
|
71
96
|
delete(key) {
|
|
72
|
-
this.
|
|
97
|
+
const node = this.map.get(key);
|
|
98
|
+
if (!node)
|
|
99
|
+
return;
|
|
100
|
+
this.removeNode(node);
|
|
101
|
+
this.map.delete(key);
|
|
73
102
|
}
|
|
74
103
|
has(key) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const entry = this.entries.get(key);
|
|
78
|
-
if (!entry)
|
|
104
|
+
const node = this.map.get(key);
|
|
105
|
+
if (!node)
|
|
79
106
|
return false;
|
|
80
|
-
if (
|
|
81
|
-
this.
|
|
107
|
+
if (node.expiresAt !== null && Date.now() > node.expiresAt) {
|
|
108
|
+
this.removeNode(node);
|
|
109
|
+
this.map.delete(key);
|
|
82
110
|
return false;
|
|
83
111
|
}
|
|
84
112
|
return true;
|
|
85
113
|
}
|
|
86
114
|
clear() {
|
|
87
|
-
this.
|
|
115
|
+
this.map.clear();
|
|
116
|
+
this.head = undefined;
|
|
117
|
+
this.tail = undefined;
|
|
88
118
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Return the count of live (non-expired) entries.
|
|
91
|
-
*
|
|
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).
|
|
95
|
-
*
|
|
96
|
-
* Two-pass to avoid mutating the Map during iteration (spec-safe).
|
|
97
|
-
*/
|
|
98
119
|
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;
|
|
120
|
+
return this.map.size;
|
|
113
121
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Stop the background cleanup timer and release internal state.
|
|
116
|
-
* Safe to call multiple times — subsequent calls are no-ops.
|
|
117
|
-
*/
|
|
118
122
|
destroy() {
|
|
119
123
|
if (this.cleanupTimer !== undefined) {
|
|
120
124
|
clearInterval(this.cleanupTimer);
|
|
121
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;
|
|
122
134
|
}
|
|
135
|
+
this.map.clear();
|
|
136
|
+
this.head = undefined;
|
|
137
|
+
this.tail = undefined;
|
|
123
138
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
appendTail(node) {
|
|
140
|
+
if (this.tail) {
|
|
141
|
+
this.tail.next = node;
|
|
142
|
+
node.prev = this.tail;
|
|
143
|
+
node.next = undefined;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.head = node;
|
|
147
|
+
}
|
|
148
|
+
this.tail = node;
|
|
149
|
+
}
|
|
150
|
+
removeNode(node) {
|
|
151
|
+
if (node.prev) {
|
|
152
|
+
node.prev.next = node.next;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.head = node.next;
|
|
156
|
+
}
|
|
157
|
+
if (node.next) {
|
|
158
|
+
node.next.prev = node.prev;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.tail = node.prev;
|
|
162
|
+
}
|
|
163
|
+
node.prev = undefined;
|
|
164
|
+
node.next = undefined;
|
|
165
|
+
}
|
|
166
|
+
moveToTail(node) {
|
|
167
|
+
if (this.tail === node)
|
|
168
|
+
return;
|
|
169
|
+
this.removeNode(node);
|
|
170
|
+
this.appendTail(node);
|
|
171
|
+
}
|
|
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
|
+
}
|
|
134
180
|
}
|
|
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
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
135
190
|
}
|
|
136
|
-
for (const key of expired)
|
|
137
|
-
this.entries.delete(key);
|
|
138
191
|
}
|
|
139
192
|
}
|
|
140
193
|
exports.InMemoryStore = InMemoryStore;
|
|
194
|
+
function createDefaultStore() {
|
|
195
|
+
return new InMemoryStore({
|
|
196
|
+
maxSize: limits_js_1.LIMITS.DEFAULT_STORE_MAX_SIZE,
|
|
197
|
+
autoCleanup: true,
|
|
198
|
+
cleanupIntervalMs: limits_js_1.LIMITS.DEFAULT_STORE_CLEANUP_INTERVAL_MS,
|
|
199
|
+
});
|
|
200
|
+
}
|