actly 1.1.0 → 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/LICENSE +0 -0
- package/README.md +262 -232
- package/dist/core/act.cjs +505 -39
- package/dist/core/act.d.ts +81 -10
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +504 -40
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +21 -9
- package/dist/core/executor.d.ts +31 -6
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +21 -9
- 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 +56 -6
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -4
- 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 +223 -22
- package/dist/policies/cache.d.ts +42 -7
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +223 -22
- 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 +98 -21
- package/dist/policies/dedupe.d.ts +20 -13
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +98 -21
- 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 +103 -26
- package/dist/policies/retry.d.ts +25 -2
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +103 -26
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +78 -36
- package/dist/policies/timeout.d.ts +7 -18
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +84 -31
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +9 -2
- package/dist/state/store.d.ts +9 -0
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +9 -2
- package/dist/state/store.js.map +1 -1
- package/dist/stores/base.cjs +4 -4
- package/dist/stores/base.d.ts +32 -56
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +4 -4
- package/dist/stores/base.js.map +1 -1
- package/dist/stores/memory.cjs +161 -39
- package/dist/stores/memory.d.ts +64 -24
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +160 -39
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +315 -37
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +212 -0
- package/dist/utils/abort.d.ts +79 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +206 -0
- package/dist/utils/abort.js.map +1 -0
- 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 +54 -0
- package/dist/utils/backoff.d.ts +14 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +52 -0
- package/dist/utils/backoff.js.map +1 -0
- 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 +153 -0
- package/dist/utils/validate.d.ts +26 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +141 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +20 -37
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.circuitBreakerPolicy = circuitBreakerPolicy;
|
|
4
|
+
const executor_js_1 = require("../core/executor.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const abort_js_1 = require("../utils/abort.js");
|
|
7
|
+
const NS = 'cb:';
|
|
8
|
+
function getState(store, key) {
|
|
9
|
+
return store.get(NS + key) ?? { failures: 0, lastFailureTime: 0, isOpen: false, openedAt: 0, halfOpen: false };
|
|
10
|
+
}
|
|
11
|
+
function setState(store, key, state) {
|
|
12
|
+
store.set(NS + key, state);
|
|
13
|
+
}
|
|
14
|
+
function circuitBreakerPolicy(opts) {
|
|
15
|
+
const threshold = Math.max(1, Math.floor(opts.threshold));
|
|
16
|
+
const cooldownMs = opts.cooldownMs;
|
|
17
|
+
const resetTimeoutMs = opts.resetTimeoutMs ?? Number.POSITIVE_INFINITY;
|
|
18
|
+
const applier = (fn, ctx) => {
|
|
19
|
+
const syncCtx = ctx;
|
|
20
|
+
return async (signal) => {
|
|
21
|
+
const key = syncCtx.key;
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
// Read state synchronously and make decisions atomically
|
|
24
|
+
const state = getState(syncCtx.store, key);
|
|
25
|
+
if (state.isOpen) {
|
|
26
|
+
const elapsed = now - state.openedAt;
|
|
27
|
+
if (elapsed >= cooldownMs) {
|
|
28
|
+
// Transition to half-open: allow only ONE probe call
|
|
29
|
+
state.isOpen = false;
|
|
30
|
+
state.halfOpen = true;
|
|
31
|
+
setState(syncCtx.store, key, state);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new errors_js_1.CircuitBreakerOpenError(key, cooldownMs - elapsed);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (state.halfOpen) {
|
|
38
|
+
// Another call is already probing — block this one
|
|
39
|
+
throw new errors_js_1.CircuitBreakerOpenError(key, 0);
|
|
40
|
+
}
|
|
41
|
+
if (now - state.lastFailureTime > resetTimeoutMs && state.failures > 0) {
|
|
42
|
+
state.failures = 0;
|
|
43
|
+
setState(syncCtx.store, key, state);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await fn(signal);
|
|
47
|
+
// Re-read state (it may have changed during await) and update atomically
|
|
48
|
+
const updated = getState(syncCtx.store, key);
|
|
49
|
+
updated.failures = 0;
|
|
50
|
+
updated.isOpen = false;
|
|
51
|
+
updated.halfOpen = false;
|
|
52
|
+
setState(syncCtx.store, key, updated);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Signal aborts are not downstream failures — don't count them
|
|
57
|
+
if ((0, abort_js_1.isAbortError)(err) || (signal.aborted && err === signal.reason)) {
|
|
58
|
+
const updated = getState(syncCtx.store, key);
|
|
59
|
+
if (updated.halfOpen) {
|
|
60
|
+
// Abort during half-open: probe didn't succeed or fail —
|
|
61
|
+
// downstream health is still unknown. Go back to OPEN with
|
|
62
|
+
// fresh cooldown so next call waits before probing again.
|
|
63
|
+
updated.isOpen = true;
|
|
64
|
+
updated.openedAt = Date.now();
|
|
65
|
+
updated.halfOpen = false;
|
|
66
|
+
}
|
|
67
|
+
setState(syncCtx.store, key, updated);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
const updated = getState(syncCtx.store, key);
|
|
71
|
+
updated.failures++;
|
|
72
|
+
updated.lastFailureTime = Date.now();
|
|
73
|
+
updated.halfOpen = false;
|
|
74
|
+
if (updated.failures >= threshold) {
|
|
75
|
+
updated.isOpen = true;
|
|
76
|
+
updated.openedAt = Date.now();
|
|
77
|
+
}
|
|
78
|
+
setState(syncCtx.store, key, updated);
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
|
|
84
|
+
return applier;
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuitBreaker.d.ts","sourceRoot":"","sources":["../../src/policies/circuitBreaker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAwBnG,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,GAAG,aAAa,CAAC,CAAC,CAAC,CA2ErF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { REQUIRES_SYNC_STORE } from '../core/executor.js';
|
|
2
|
+
import { CircuitBreakerOpenError } from '../errors.js';
|
|
3
|
+
import { isAbortError } from '../utils/abort.js';
|
|
4
|
+
const NS = 'cb:';
|
|
5
|
+
function getState(store, key) {
|
|
6
|
+
return store.get(NS + key) ?? { failures: 0, lastFailureTime: 0, isOpen: false, openedAt: 0, halfOpen: false };
|
|
7
|
+
}
|
|
8
|
+
function setState(store, key, state) {
|
|
9
|
+
store.set(NS + key, state);
|
|
10
|
+
}
|
|
11
|
+
export function circuitBreakerPolicy(opts) {
|
|
12
|
+
const threshold = Math.max(1, Math.floor(opts.threshold));
|
|
13
|
+
const cooldownMs = opts.cooldownMs;
|
|
14
|
+
const resetTimeoutMs = opts.resetTimeoutMs ?? Number.POSITIVE_INFINITY;
|
|
15
|
+
const applier = (fn, ctx) => {
|
|
16
|
+
const syncCtx = ctx;
|
|
17
|
+
return async (signal) => {
|
|
18
|
+
const key = syncCtx.key;
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
// Read state synchronously and make decisions atomically
|
|
21
|
+
const state = getState(syncCtx.store, key);
|
|
22
|
+
if (state.isOpen) {
|
|
23
|
+
const elapsed = now - state.openedAt;
|
|
24
|
+
if (elapsed >= cooldownMs) {
|
|
25
|
+
// Transition to half-open: allow only ONE probe call
|
|
26
|
+
state.isOpen = false;
|
|
27
|
+
state.halfOpen = true;
|
|
28
|
+
setState(syncCtx.store, key, state);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
throw new CircuitBreakerOpenError(key, cooldownMs - elapsed);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (state.halfOpen) {
|
|
35
|
+
// Another call is already probing — block this one
|
|
36
|
+
throw new CircuitBreakerOpenError(key, 0);
|
|
37
|
+
}
|
|
38
|
+
if (now - state.lastFailureTime > resetTimeoutMs && state.failures > 0) {
|
|
39
|
+
state.failures = 0;
|
|
40
|
+
setState(syncCtx.store, key, state);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const result = await fn(signal);
|
|
44
|
+
// Re-read state (it may have changed during await) and update atomically
|
|
45
|
+
const updated = getState(syncCtx.store, key);
|
|
46
|
+
updated.failures = 0;
|
|
47
|
+
updated.isOpen = false;
|
|
48
|
+
updated.halfOpen = false;
|
|
49
|
+
setState(syncCtx.store, key, updated);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// Signal aborts are not downstream failures — don't count them
|
|
54
|
+
if (isAbortError(err) || (signal.aborted && err === signal.reason)) {
|
|
55
|
+
const updated = getState(syncCtx.store, key);
|
|
56
|
+
if (updated.halfOpen) {
|
|
57
|
+
// Abort during half-open: probe didn't succeed or fail —
|
|
58
|
+
// downstream health is still unknown. Go back to OPEN with
|
|
59
|
+
// fresh cooldown so next call waits before probing again.
|
|
60
|
+
updated.isOpen = true;
|
|
61
|
+
updated.openedAt = Date.now();
|
|
62
|
+
updated.halfOpen = false;
|
|
63
|
+
}
|
|
64
|
+
setState(syncCtx.store, key, updated);
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
const updated = getState(syncCtx.store, key);
|
|
68
|
+
updated.failures++;
|
|
69
|
+
updated.lastFailureTime = Date.now();
|
|
70
|
+
updated.halfOpen = false;
|
|
71
|
+
if (updated.failures >= threshold) {
|
|
72
|
+
updated.isOpen = true;
|
|
73
|
+
updated.openedAt = Date.now();
|
|
74
|
+
}
|
|
75
|
+
setState(syncCtx.store, key, updated);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
applier[REQUIRES_SYNC_STORE] = true;
|
|
81
|
+
return applier;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=circuitBreaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuitBreaker.js","sourceRoot":"","sources":["../../src/policies/circuitBreaker.ts"],"names":[],"mappings":"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"}
|
package/dist/policies/dedupe.cjs
CHANGED
|
@@ -2,38 +2,115 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dedupePolicy = dedupePolicy;
|
|
4
4
|
const executor_js_1 = require("../core/executor.js");
|
|
5
|
-
|
|
5
|
+
const abort_js_1 = require("../utils/abort.js");
|
|
6
|
+
// Namespace so dedupe keys never collide with cache keys in the shared store.
|
|
6
7
|
const NS = 'dedupe:';
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Module-level generation counter. Monotonic — guarantees uniqueness
|
|
10
|
+
* across the lifetime of the process. Wraps at `Number.MAX_SAFE_INTEGER`
|
|
11
|
+
* (which would take ~285 000 years at 1M increments/sec).
|
|
12
|
+
*/
|
|
13
|
+
let generationCounter = 0;
|
|
14
|
+
function nextGeneration() {
|
|
15
|
+
generationCounter = (generationCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
16
|
+
return generationCounter;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Collapse concurrent calls that share the same key into one in-flight Promise.
|
|
9
20
|
*
|
|
10
|
-
*
|
|
11
|
-
* the first resolves gets the same Promise back — no duplicate work.
|
|
21
|
+
* # Properties
|
|
12
22
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* - **Generation-safe cleanup**: stale originators never delete newer
|
|
24
|
+
* entries when `inflightTtl` triggers replacement.
|
|
25
|
+
* - **Joiner isolation**: originator's caller-signal abort does NOT
|
|
26
|
+
* propagate to joiners. Each joiner races the shared in-flight promise
|
|
27
|
+
* against their OWN signal only.
|
|
28
|
+
* - **Truthful joiner attempts**: joiners that abort before the originator
|
|
29
|
+
* settles report `attempts: 0` (they did no work), not the originator's
|
|
30
|
+
* in-progress count.
|
|
31
|
+
*
|
|
32
|
+
* # INVARIANT: requires SyncStateStore
|
|
19
33
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
34
|
+
* The read-then-write that makes deduplication work must happen in a single
|
|
35
|
+
* synchronous frame. An async store would introduce an `await` between
|
|
36
|
+
* `get()` and `set()`, letting two concurrent callers both see a miss and
|
|
37
|
+
* both launch work. The `REQUIRES_SYNC_STORE` symbol on the returned
|
|
38
|
+
* `PolicyApplier` lets `execute()` enforce this at runtime for JS callers
|
|
39
|
+
* that bypass TypeScript.
|
|
22
40
|
*/
|
|
23
|
-
function dedupePolicy() {
|
|
41
|
+
function dedupePolicy(opts = { enabled: true }) {
|
|
42
|
+
const inflightTtl = opts.inflightTtl;
|
|
24
43
|
const applier = (fn, ctx) => {
|
|
25
44
|
// Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
|
|
26
45
|
// any policy tagged with REQUIRES_SYNC_STORE.
|
|
27
46
|
const syncCtx = ctx;
|
|
28
|
-
return async () => {
|
|
47
|
+
return async (signal) => {
|
|
29
48
|
const key = NS + syncCtx.key;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
// ─── Fast path: an in-flight promise already exists. Join it. ──────
|
|
50
|
+
//
|
|
51
|
+
// The joiner races the in-flight promise against THEIR OWN signal.
|
|
52
|
+
// They do NOT inherit the originator's signal state — if the
|
|
53
|
+
// originator's caller cancels, joiners continue waiting (or get
|
|
54
|
+
// the eventual settled value).
|
|
55
|
+
const existing = syncCtx.store.get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
try {
|
|
58
|
+
const value = await (0, abort_js_1.raceAbort)(existing.promise, signal);
|
|
59
|
+
// Success — copy originator's final meta (attempts, source)
|
|
60
|
+
// so the joiner's ActResult reflects the real effort.
|
|
61
|
+
syncCtx.meta.attempts = existing.meta.attempts;
|
|
62
|
+
syncCtx.meta.source = existing.meta.source;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Joiner either aborted (their own signal) or got the
|
|
67
|
+
// originator's settled error.
|
|
68
|
+
if (signal.aborted) {
|
|
69
|
+
// Joiner's own signal aborted before originator settled.
|
|
70
|
+
// They did no work — report `attempts: 0` (truthful).
|
|
71
|
+
syncCtx.meta.attempts = 0;
|
|
72
|
+
// source stays 'fresh' (default) — joiner didn't read from cache
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Originator settled with failure — copy its final meta.
|
|
76
|
+
syncCtx.meta.attempts = existing.meta.attempts;
|
|
77
|
+
syncCtx.meta.source = existing.meta.source;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ─── Originator path: start the work and publish the promise. ──────
|
|
83
|
+
//
|
|
84
|
+
// The stored promise is the RAW fn(signal) — NOT raceAbort-wrapped.
|
|
85
|
+
// This is critical: if we stored raceAbort(fn, signal), an
|
|
86
|
+
// originator signal abort would cause the stored promise to reject,
|
|
87
|
+
// which would propagate to all joiners. Instead:
|
|
88
|
+
// - stored: raw fn(signal) — joiners await this
|
|
89
|
+
// - originator's await: raceAbort(stored, signal) — originator
|
|
90
|
+
// can bail out on their own signal without affecting joiners
|
|
91
|
+
const generation = nextGeneration();
|
|
92
|
+
const rawPromise = Promise.resolve(fn(signal));
|
|
93
|
+
const entry = {
|
|
94
|
+
promise: rawPromise,
|
|
95
|
+
meta: syncCtx.meta,
|
|
96
|
+
generation,
|
|
97
|
+
};
|
|
98
|
+
syncCtx.store.set(key, entry, inflightTtl);
|
|
99
|
+
// Cleanup on settle: only delete if generation matches. If a newer
|
|
100
|
+
// originator has replaced this entry (because inflightTtl expired),
|
|
101
|
+
// our cleanup is a no-op — the newer entry stays.
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
const current = syncCtx.store.get(key);
|
|
104
|
+
if (current && current.generation === generation) {
|
|
105
|
+
syncCtx.store.delete(key);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
// Attach cleanup to the RAW promise (not the raceAbort-wrapped one)
|
|
109
|
+
// so cleanup runs whenever fn settles, regardless of originator abort.
|
|
110
|
+
rawPromise.then(cleanup, cleanup);
|
|
111
|
+
// Originator races the raw promise against their own signal —
|
|
112
|
+
// they can bail out without affecting joiners.
|
|
113
|
+
return (0, abort_js_1.raceAbort)(rawPromise, signal);
|
|
37
114
|
};
|
|
38
115
|
};
|
|
39
116
|
applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
|
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import type { PolicyApplier } from '../types/index.js';
|
|
1
|
+
import type { PolicyApplier, DedupeOptions } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Collapse concurrent calls that share the same key into one in-flight Promise.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* the first resolves gets the same Promise back — no duplicate work.
|
|
5
|
+
* # Properties
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* - **Generation-safe cleanup**: stale originators never delete newer
|
|
8
|
+
* entries when `inflightTtl` triggers replacement.
|
|
9
|
+
* - **Joiner isolation**: originator's caller-signal abort does NOT
|
|
10
|
+
* propagate to joiners. Each joiner races the shared in-flight promise
|
|
11
|
+
* against their OWN signal only.
|
|
12
|
+
* - **Truthful joiner attempts**: joiners that abort before the originator
|
|
13
|
+
* settles report `attempts: 0` (they did no work), not the originator's
|
|
14
|
+
* in-progress count.
|
|
15
|
+
*
|
|
16
|
+
* # INVARIANT: requires SyncStateStore
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* The read-then-write that makes deduplication work must happen in a single
|
|
19
|
+
* synchronous frame. An async store would introduce an `await` between
|
|
20
|
+
* `get()` and `set()`, letting two concurrent callers both see a miss and
|
|
21
|
+
* both launch work. The `REQUIRES_SYNC_STORE` symbol on the returned
|
|
22
|
+
* `PolicyApplier` lets `execute()` enforce this at runtime for JS callers
|
|
23
|
+
* that bypass TypeScript.
|
|
17
24
|
*/
|
|
18
|
-
export declare function dedupePolicy<T>(): PolicyApplier<T>;
|
|
25
|
+
export declare function dedupePolicy<T>(opts?: DedupeOptions): PolicyApplier<T>;
|
|
19
26
|
//# sourceMappingURL=dedupe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,aAAa,EAAE,MAAM,mBAAmB,CAAA;AA6D3F;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,GAAE,aAAiC,GAAG,aAAa,CAAC,CAAC,CAAC,CAqFzF"}
|
package/dist/policies/dedupe.js
CHANGED
|
@@ -1,36 +1,113 @@
|
|
|
1
1
|
import { REQUIRES_SYNC_STORE } from '../core/executor.js';
|
|
2
|
-
|
|
2
|
+
import { raceAbort } from '../utils/abort.js';
|
|
3
|
+
// Namespace so dedupe keys never collide with cache keys in the shared store.
|
|
3
4
|
const NS = 'dedupe:';
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* Module-level generation counter. Monotonic — guarantees uniqueness
|
|
7
|
+
* across the lifetime of the process. Wraps at `Number.MAX_SAFE_INTEGER`
|
|
8
|
+
* (which would take ~285 000 years at 1M increments/sec).
|
|
9
|
+
*/
|
|
10
|
+
let generationCounter = 0;
|
|
11
|
+
function nextGeneration() {
|
|
12
|
+
generationCounter = (generationCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
13
|
+
return generationCounter;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Collapse concurrent calls that share the same key into one in-flight Promise.
|
|
6
17
|
*
|
|
7
|
-
*
|
|
8
|
-
* the first resolves gets the same Promise back — no duplicate work.
|
|
18
|
+
* # Properties
|
|
9
19
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
20
|
+
* - **Generation-safe cleanup**: stale originators never delete newer
|
|
21
|
+
* entries when `inflightTtl` triggers replacement.
|
|
22
|
+
* - **Joiner isolation**: originator's caller-signal abort does NOT
|
|
23
|
+
* propagate to joiners. Each joiner races the shared in-flight promise
|
|
24
|
+
* against their OWN signal only.
|
|
25
|
+
* - **Truthful joiner attempts**: joiners that abort before the originator
|
|
26
|
+
* settles report `attempts: 0` (they did no work), not the originator's
|
|
27
|
+
* in-progress count.
|
|
28
|
+
*
|
|
29
|
+
* # INVARIANT: requires SyncStateStore
|
|
16
30
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
31
|
+
* The read-then-write that makes deduplication work must happen in a single
|
|
32
|
+
* synchronous frame. An async store would introduce an `await` between
|
|
33
|
+
* `get()` and `set()`, letting two concurrent callers both see a miss and
|
|
34
|
+
* both launch work. The `REQUIRES_SYNC_STORE` symbol on the returned
|
|
35
|
+
* `PolicyApplier` lets `execute()` enforce this at runtime for JS callers
|
|
36
|
+
* that bypass TypeScript.
|
|
19
37
|
*/
|
|
20
|
-
export function dedupePolicy() {
|
|
38
|
+
export function dedupePolicy(opts = { enabled: true }) {
|
|
39
|
+
const inflightTtl = opts.inflightTtl;
|
|
21
40
|
const applier = (fn, ctx) => {
|
|
22
41
|
// Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
|
|
23
42
|
// any policy tagged with REQUIRES_SYNC_STORE.
|
|
24
43
|
const syncCtx = ctx;
|
|
25
|
-
return async () => {
|
|
44
|
+
return async (signal) => {
|
|
26
45
|
const key = NS + syncCtx.key;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
// ─── Fast path: an in-flight promise already exists. Join it. ──────
|
|
47
|
+
//
|
|
48
|
+
// The joiner races the in-flight promise against THEIR OWN signal.
|
|
49
|
+
// They do NOT inherit the originator's signal state — if the
|
|
50
|
+
// originator's caller cancels, joiners continue waiting (or get
|
|
51
|
+
// the eventual settled value).
|
|
52
|
+
const existing = syncCtx.store.get(key);
|
|
53
|
+
if (existing) {
|
|
54
|
+
try {
|
|
55
|
+
const value = await raceAbort(existing.promise, signal);
|
|
56
|
+
// Success — copy originator's final meta (attempts, source)
|
|
57
|
+
// so the joiner's ActResult reflects the real effort.
|
|
58
|
+
syncCtx.meta.attempts = existing.meta.attempts;
|
|
59
|
+
syncCtx.meta.source = existing.meta.source;
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// Joiner either aborted (their own signal) or got the
|
|
64
|
+
// originator's settled error.
|
|
65
|
+
if (signal.aborted) {
|
|
66
|
+
// Joiner's own signal aborted before originator settled.
|
|
67
|
+
// They did no work — report `attempts: 0` (truthful).
|
|
68
|
+
syncCtx.meta.attempts = 0;
|
|
69
|
+
// source stays 'fresh' (default) — joiner didn't read from cache
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Originator settled with failure — copy its final meta.
|
|
73
|
+
syncCtx.meta.attempts = existing.meta.attempts;
|
|
74
|
+
syncCtx.meta.source = existing.meta.source;
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ─── Originator path: start the work and publish the promise. ──────
|
|
80
|
+
//
|
|
81
|
+
// The stored promise is the RAW fn(signal) — NOT raceAbort-wrapped.
|
|
82
|
+
// This is critical: if we stored raceAbort(fn, signal), an
|
|
83
|
+
// originator signal abort would cause the stored promise to reject,
|
|
84
|
+
// which would propagate to all joiners. Instead:
|
|
85
|
+
// - stored: raw fn(signal) — joiners await this
|
|
86
|
+
// - originator's await: raceAbort(stored, signal) — originator
|
|
87
|
+
// can bail out on their own signal without affecting joiners
|
|
88
|
+
const generation = nextGeneration();
|
|
89
|
+
const rawPromise = Promise.resolve(fn(signal));
|
|
90
|
+
const entry = {
|
|
91
|
+
promise: rawPromise,
|
|
92
|
+
meta: syncCtx.meta,
|
|
93
|
+
generation,
|
|
94
|
+
};
|
|
95
|
+
syncCtx.store.set(key, entry, inflightTtl);
|
|
96
|
+
// Cleanup on settle: only delete if generation matches. If a newer
|
|
97
|
+
// originator has replaced this entry (because inflightTtl expired),
|
|
98
|
+
// our cleanup is a no-op — the newer entry stays.
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
const current = syncCtx.store.get(key);
|
|
101
|
+
if (current && current.generation === generation) {
|
|
102
|
+
syncCtx.store.delete(key);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// Attach cleanup to the RAW promise (not the raceAbort-wrapped one)
|
|
106
|
+
// so cleanup runs whenever fn settles, regardless of originator abort.
|
|
107
|
+
rawPromise.then(cleanup, cleanup);
|
|
108
|
+
// Originator races the raw promise against their own signal —
|
|
109
|
+
// they can bail out without affecting joiners.
|
|
110
|
+
return raceAbort(rawPromise, signal);
|
|
34
111
|
};
|
|
35
112
|
};
|
|
36
113
|
applier[REQUIRES_SYNC_STORE] = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rateLimitPolicy = rateLimitPolicy;
|
|
4
|
+
const executor_js_1 = require("../core/executor.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const NS = 'rl:';
|
|
7
|
+
function getState(store, key) {
|
|
8
|
+
return store.get(NS + key) ?? { timestamps: [] };
|
|
9
|
+
}
|
|
10
|
+
function setState(store, key, state) {
|
|
11
|
+
store.set(NS + key, state);
|
|
12
|
+
}
|
|
13
|
+
function rateLimitPolicy(opts) {
|
|
14
|
+
const maxCalls = Math.max(1, Math.floor(opts.maxCalls));
|
|
15
|
+
const windowMs = opts.windowMs;
|
|
16
|
+
const applier = (fn, ctx) => {
|
|
17
|
+
const syncCtx = ctx;
|
|
18
|
+
return async (signal) => {
|
|
19
|
+
const key = syncCtx.key;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const state = getState(syncCtx.store, key);
|
|
22
|
+
const cutoff = now - windowMs;
|
|
23
|
+
state.timestamps = state.timestamps.filter(t => t > cutoff);
|
|
24
|
+
if (state.timestamps.length >= maxCalls) {
|
|
25
|
+
setState(syncCtx.store, key, state);
|
|
26
|
+
throw new errors_js_1.RateLimitError(key, maxCalls, windowMs);
|
|
27
|
+
}
|
|
28
|
+
state.timestamps.push(now);
|
|
29
|
+
setState(syncCtx.store, key, state);
|
|
30
|
+
return fn(signal);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
|
|
34
|
+
return applier;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/policies/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAmB9F,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,gBAAgB,GAAG,aAAa,CAAC,CAAC,CAAC,CA6B3E"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { REQUIRES_SYNC_STORE } from '../core/executor.js';
|
|
2
|
+
import { RateLimitError } from '../errors.js';
|
|
3
|
+
const NS = 'rl:';
|
|
4
|
+
function getState(store, key) {
|
|
5
|
+
return store.get(NS + key) ?? { timestamps: [] };
|
|
6
|
+
}
|
|
7
|
+
function setState(store, key, state) {
|
|
8
|
+
store.set(NS + key, state);
|
|
9
|
+
}
|
|
10
|
+
export function rateLimitPolicy(opts) {
|
|
11
|
+
const maxCalls = Math.max(1, Math.floor(opts.maxCalls));
|
|
12
|
+
const windowMs = opts.windowMs;
|
|
13
|
+
const applier = (fn, ctx) => {
|
|
14
|
+
const syncCtx = ctx;
|
|
15
|
+
return async (signal) => {
|
|
16
|
+
const key = syncCtx.key;
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const state = getState(syncCtx.store, key);
|
|
19
|
+
const cutoff = now - windowMs;
|
|
20
|
+
state.timestamps = state.timestamps.filter(t => t > cutoff);
|
|
21
|
+
if (state.timestamps.length >= maxCalls) {
|
|
22
|
+
setState(syncCtx.store, key, state);
|
|
23
|
+
throw new RateLimitError(key, maxCalls, windowMs);
|
|
24
|
+
}
|
|
25
|
+
state.timestamps.push(now);
|
|
26
|
+
setState(syncCtx.store, key, state);
|
|
27
|
+
return fn(signal);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
applier[REQUIRES_SYNC_STORE] = true;
|
|
31
|
+
return applier;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=rateLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/policies/rateLimit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,EAAE,GAAG,KAAK,CAAA;AAMhB,SAAS,QAAQ,CAAC,KAAqB,EAAE,GAAW;IAClD,OAAO,KAAK,CAAC,GAAG,CAAiB,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;AAClE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAqB,EAAE,GAAW,EAAE,KAAqB;IACzE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,IAAsB;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;IAE9B,MAAM,OAAO,GAAG,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE;QAC7D,MAAM,OAAO,GAAG,GAA+D,CAAA;QAE/E,OAAO,KAAK,EAAE,MAAmB,EAAE,EAAE;YACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAA;YACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YAE1C,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAA;YAC7B,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAA;YAE3D,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;gBACxC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;gBACnC,MAAM,IAAI,cAAc,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;YACnD,CAAC;YAED,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC1B,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;YAEnC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAA;QACnB,CAAC,CAAA;IACH,CAAC,CAEA;IAAC,OAA+D,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAA;IAC7F,OAAO,OAAO,CAAA;AAChB,CAAC"}
|