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.
Files changed (130) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +262 -232
  3. package/dist/core/act.cjs +505 -39
  4. package/dist/core/act.d.ts +81 -10
  5. package/dist/core/act.d.ts.map +1 -1
  6. package/dist/core/act.js +504 -40
  7. package/dist/core/act.js.map +1 -1
  8. package/dist/core/executor.cjs +21 -9
  9. package/dist/core/executor.d.ts +31 -6
  10. package/dist/core/executor.d.ts.map +1 -1
  11. package/dist/core/executor.js +21 -9
  12. package/dist/core/executor.js.map +1 -1
  13. package/dist/core/health.cjs +34 -0
  14. package/dist/core/health.d.ts +18 -0
  15. package/dist/core/health.d.ts.map +1 -0
  16. package/dist/core/health.js +28 -0
  17. package/dist/core/health.js.map +1 -0
  18. package/dist/core/shutdown.cjs +60 -0
  19. package/dist/core/shutdown.d.ts +8 -0
  20. package/dist/core/shutdown.d.ts.map +1 -0
  21. package/dist/core/shutdown.js +56 -0
  22. package/dist/core/shutdown.js.map +1 -0
  23. package/dist/core/tenant.cjs +71 -0
  24. package/dist/core/tenant.d.ts +29 -0
  25. package/dist/core/tenant.d.ts.map +1 -0
  26. package/dist/core/tenant.js +68 -0
  27. package/dist/core/tenant.js.map +1 -0
  28. package/dist/errors.cjs +182 -0
  29. package/dist/errors.d.ts +151 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +171 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/index.cjs +56 -6
  34. package/dist/index.d.ts +22 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +24 -4
  37. package/dist/index.js.map +1 -1
  38. package/dist/observability.cjs +45 -0
  39. package/dist/observability.d.ts +145 -0
  40. package/dist/observability.d.ts.map +1 -0
  41. package/dist/observability.js +43 -0
  42. package/dist/observability.js.map +1 -0
  43. package/dist/policies/bulkhead.cjs +97 -0
  44. package/dist/policies/bulkhead.d.ts +3 -0
  45. package/dist/policies/bulkhead.d.ts.map +1 -0
  46. package/dist/policies/bulkhead.js +95 -0
  47. package/dist/policies/bulkhead.js.map +1 -0
  48. package/dist/policies/cache.cjs +223 -22
  49. package/dist/policies/cache.d.ts +42 -7
  50. package/dist/policies/cache.d.ts.map +1 -1
  51. package/dist/policies/cache.js +223 -22
  52. package/dist/policies/cache.js.map +1 -1
  53. package/dist/policies/circuitBreaker.cjs +85 -0
  54. package/dist/policies/circuitBreaker.d.ts +3 -0
  55. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  56. package/dist/policies/circuitBreaker.js +83 -0
  57. package/dist/policies/circuitBreaker.js.map +1 -0
  58. package/dist/policies/dedupe.cjs +98 -21
  59. package/dist/policies/dedupe.d.ts +20 -13
  60. package/dist/policies/dedupe.d.ts.map +1 -1
  61. package/dist/policies/dedupe.js +98 -21
  62. package/dist/policies/dedupe.js.map +1 -1
  63. package/dist/policies/rateLimit.cjs +35 -0
  64. package/dist/policies/rateLimit.d.ts +3 -0
  65. package/dist/policies/rateLimit.d.ts.map +1 -0
  66. package/dist/policies/rateLimit.js +33 -0
  67. package/dist/policies/rateLimit.js.map +1 -0
  68. package/dist/policies/retry.cjs +103 -26
  69. package/dist/policies/retry.d.ts +25 -2
  70. package/dist/policies/retry.d.ts.map +1 -1
  71. package/dist/policies/retry.js +103 -26
  72. package/dist/policies/retry.js.map +1 -1
  73. package/dist/policies/timeout.cjs +78 -36
  74. package/dist/policies/timeout.d.ts +7 -18
  75. package/dist/policies/timeout.d.ts.map +1 -1
  76. package/dist/policies/timeout.js +84 -31
  77. package/dist/policies/timeout.js.map +1 -1
  78. package/dist/state/store.cjs +9 -2
  79. package/dist/state/store.d.ts +9 -0
  80. package/dist/state/store.d.ts.map +1 -1
  81. package/dist/state/store.js +9 -2
  82. package/dist/state/store.js.map +1 -1
  83. package/dist/stores/base.cjs +4 -4
  84. package/dist/stores/base.d.ts +32 -56
  85. package/dist/stores/base.d.ts.map +1 -1
  86. package/dist/stores/base.js +4 -4
  87. package/dist/stores/base.js.map +1 -1
  88. package/dist/stores/memory.cjs +161 -39
  89. package/dist/stores/memory.d.ts +64 -24
  90. package/dist/stores/memory.d.ts.map +1 -1
  91. package/dist/stores/memory.js +160 -39
  92. package/dist/stores/memory.js.map +1 -1
  93. package/dist/types/index.d.ts +315 -37
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/utils/abort.cjs +212 -0
  96. package/dist/utils/abort.d.ts +79 -0
  97. package/dist/utils/abort.d.ts.map +1 -0
  98. package/dist/utils/abort.js +206 -0
  99. package/dist/utils/abort.js.map +1 -0
  100. package/dist/utils/abortPool.cjs +26 -0
  101. package/dist/utils/abortPool.d.ts +4 -0
  102. package/dist/utils/abortPool.d.ts.map +1 -0
  103. package/dist/utils/abortPool.js +22 -0
  104. package/dist/utils/abortPool.js.map +1 -0
  105. package/dist/utils/backoff.cjs +54 -0
  106. package/dist/utils/backoff.d.ts +14 -0
  107. package/dist/utils/backoff.d.ts.map +1 -0
  108. package/dist/utils/backoff.js +52 -0
  109. package/dist/utils/backoff.js.map +1 -0
  110. package/dist/utils/key.cjs +77 -0
  111. package/dist/utils/key.d.ts +29 -0
  112. package/dist/utils/key.d.ts.map +1 -0
  113. package/dist/utils/key.js +75 -0
  114. package/dist/utils/key.js.map +1 -0
  115. package/dist/utils/limits.cjs +40 -0
  116. package/dist/utils/limits.d.ts +39 -0
  117. package/dist/utils/limits.d.ts.map +1 -0
  118. package/dist/utils/limits.js +38 -0
  119. package/dist/utils/limits.js.map +1 -0
  120. package/dist/utils/sanitize.cjs +29 -0
  121. package/dist/utils/sanitize.d.ts +3 -0
  122. package/dist/utils/sanitize.d.ts.map +1 -0
  123. package/dist/utils/sanitize.js +26 -0
  124. package/dist/utils/sanitize.js.map +1 -0
  125. package/dist/utils/validate.cjs +153 -0
  126. package/dist/utils/validate.d.ts +26 -0
  127. package/dist/utils/validate.d.ts.map +1 -0
  128. package/dist/utils/validate.js +141 -0
  129. package/dist/utils/validate.js.map +1 -0
  130. 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,3 @@
1
+ import type { PolicyApplier, CircuitBreakerOptions } from '../types/index.js';
2
+ export declare function circuitBreakerPolicy<T>(opts: CircuitBreakerOptions): PolicyApplier<T>;
3
+ //# sourceMappingURL=circuitBreaker.d.ts.map
@@ -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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"}
@@ -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
- // Namespace so dedupe keys never collide with cache keys in the shared store
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
- * Collapses concurrent calls that share the same key into one in-flight Promise.
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
- * The first caller starts the work. Every subsequent caller that arrives before
11
- * the first resolves gets the same Promise back — no duplicate work.
21
+ * # Properties
12
22
  *
13
- * INVARIANT: requires a SyncStateStore see stores/base.ts.
14
- * The read-then-write that makes deduplication work must happen in a single
15
- * synchronous frame. An async store would introduce an await between get() and
16
- * set(), letting two concurrent callers both see a miss and both launch work.
17
- * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
18
- * enforce this at runtime for JS callers that bypass TypeScript.
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
- * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
21
- * the retry counter belongs to the originating call's meta object.
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
- const inflight = syncCtx.store.get(key);
31
- if (inflight)
32
- return inflight;
33
- // No TTL .finally() cleans up regardless of outcome
34
- const promise = fn().finally(() => syncCtx.store.delete(key));
35
- syncCtx.store.set(key, promise);
36
- return promise;
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
- * Collapses concurrent calls that share the same key into one in-flight Promise.
3
+ * Collapse concurrent calls that share the same key into one in-flight Promise.
4
4
  *
5
- * The first caller starts the work. Every subsequent caller that arrives before
6
- * the first resolves gets the same Promise back — no duplicate work.
5
+ * # Properties
7
6
  *
8
- * INVARIANT: requires a SyncStateStore see stores/base.ts.
9
- * The read-then-write that makes deduplication work must happen in a single
10
- * synchronous frame. An async store would introduce an await between get() and
11
- * set(), letting two concurrent callers both see a miss and both launch work.
12
- * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
13
- * enforce this at runtime for JS callers that bypass TypeScript.
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
- * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
16
- * the retry counter belongs to the originating call's meta object.
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;AAY5E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAuBlD"}
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"}
@@ -1,36 +1,113 @@
1
1
  import { REQUIRES_SYNC_STORE } from '../core/executor.js';
2
- // Namespace so dedupe keys never collide with cache keys in the shared store
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
- * Collapses concurrent calls that share the same key into one in-flight Promise.
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
- * The first caller starts the work. Every subsequent caller that arrives before
8
- * the first resolves gets the same Promise back — no duplicate work.
18
+ * # Properties
9
19
  *
10
- * INVARIANT: requires a SyncStateStore see stores/base.ts.
11
- * The read-then-write that makes deduplication work must happen in a single
12
- * synchronous frame. An async store would introduce an await between get() and
13
- * set(), letting two concurrent callers both see a miss and both launch work.
14
- * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
15
- * enforce this at runtime for JS callers that bypass TypeScript.
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
- * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
18
- * the retry counter belongs to the originating call's meta object.
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
- const inflight = syncCtx.store.get(key);
28
- if (inflight)
29
- return inflight;
30
- // No TTL .finally() cleans up regardless of outcome
31
- const promise = fn().finally(() => syncCtx.store.delete(key));
32
- syncCtx.store.set(key, promise);
33
- return promise;
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;AAEzD,6EAA6E;AAC7E,MAAM,EAAE,GAAG,SAAS,CAAA;AAOpB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE;QAC7D,yEAAyE;QACzE,8CAA8C;QAC9C,MAAM,OAAO,GAAG,GAAoB,CAAA;QAEpC,OAAO,KAAK,IAAI,EAAE;YAChB,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,CAAA;YAE5B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,CAAC,CAAA;YACnD,IAAI,QAAQ;gBAAE,OAAO,QAAQ,CAAA;YAE7B,sDAAsD;YACtD,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC7D,OAAO,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,EAAE,OAAO,CAAC,CAAA;YAC3C,OAAO,OAAO,CAAA;QAChB,CAAC,CAAA;IACH,CAAC,CAGA;IAAC,OAA+D,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAA;IAE7F,OAAO,OAAO,CAAA;AAChB,CAAC"}
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,3 @@
1
+ import type { PolicyApplier, RateLimitOptions } from '../types/index.js';
2
+ export declare function rateLimitPolicy<T>(opts: RateLimitOptions): PolicyApplier<T>;
3
+ //# sourceMappingURL=rateLimit.d.ts.map
@@ -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"}