actly 1.0.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/core/act.cjs +71 -0
  3. package/dist/core/act.d.ts +27 -0
  4. package/dist/core/act.d.ts.map +1 -0
  5. package/dist/core/act.js +69 -0
  6. package/dist/core/act.js.map +1 -0
  7. package/dist/core/executor.cjs +21 -0
  8. package/dist/core/executor.d.ts +27 -0
  9. package/dist/core/executor.d.ts.map +1 -0
  10. package/dist/core/executor.js +19 -0
  11. package/dist/core/executor.js.map +1 -0
  12. package/dist/index.cjs +12 -0
  13. package/dist/index.d.ts +5 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +6 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/policies/cache.cjs +23 -0
  18. package/dist/policies/cache.d.ts +9 -0
  19. package/dist/policies/cache.d.ts.map +1 -0
  20. package/dist/policies/cache.js +21 -0
  21. package/dist/policies/cache.js.map +1 -0
  22. package/dist/policies/dedupe.cjs +26 -0
  23. package/dist/policies/dedupe.d.ts +12 -0
  24. package/dist/policies/dedupe.d.ts.map +1 -0
  25. package/dist/policies/dedupe.js +24 -0
  26. package/dist/policies/dedupe.js.map +1 -0
  27. package/dist/policies/retry.cjs +47 -0
  28. package/dist/policies/retry.d.ts +7 -0
  29. package/dist/policies/retry.d.ts.map +1 -0
  30. package/dist/policies/retry.js +45 -0
  31. package/dist/policies/retry.js.map +1 -0
  32. package/dist/policies/timeout.cjs +57 -0
  33. package/dist/policies/timeout.d.ts +30 -0
  34. package/dist/policies/timeout.d.ts.map +1 -0
  35. package/dist/policies/timeout.js +51 -0
  36. package/dist/policies/timeout.js.map +1 -0
  37. package/dist/state/store.cjs +41 -0
  38. package/dist/state/store.d.ts +13 -0
  39. package/dist/state/store.d.ts.map +1 -0
  40. package/dist/state/store.js +38 -0
  41. package/dist/state/store.js.map +1 -0
  42. package/dist/types/index.cjs +3 -0
  43. package/dist/types/index.d.ts +116 -0
  44. package/dist/types/index.d.ts.map +1 -0
  45. package/dist/types/index.js +3 -0
  46. package/dist/types/index.js.map +1 -0
  47. package/package.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ACT contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.act = act;
4
+ const executor_js_1 = require("./executor.js");
5
+ const retry_js_1 = require("../policies/retry.js");
6
+ const timeout_js_1 = require("../policies/timeout.js");
7
+ const dedupe_js_1 = require("../policies/dedupe.js");
8
+ const cache_js_1 = require("../policies/cache.js");
9
+ const store_js_1 = require("../state/store.js");
10
+ // Module-level default store so cache and dedupe persist across calls.
11
+ // For SSR isolation or per-test control, construct an InMemoryStore and
12
+ // call execute() directly — it accepts any StateStore implementation.
13
+ const defaultStore = new store_js_1.InMemoryStore();
14
+ /**
15
+ * Execute fn with the given reliability policies.
16
+ *
17
+ * @param key Stable identifier for this action. Scopes dedupe + cache.
18
+ * @param fn The async work to run.
19
+ * @param options Which policies to apply and how. All fields are optional.
20
+ *
21
+ * @returns ActResult<T> — always resolves, never throws.
22
+ * Check result.ok before reading result.value.
23
+ *
24
+ * @example
25
+ * const result = await act('user:42', () => fetchUser(42), {
26
+ * retry: { attempts: 3, delayMs: 200, backoff: 'exponential' },
27
+ * timeout: { ms: 5_000 },
28
+ * dedupe: true,
29
+ * cache: { ttl: 60_000 },
30
+ * })
31
+ *
32
+ * if (result.ok) {
33
+ * console.log(result.value, result.source, result.attempts)
34
+ * } else {
35
+ * console.error(result.error)
36
+ * }
37
+ */
38
+ async function act(key, fn, options = {}) {
39
+ const meta = { attempts: 1, source: 'fresh' };
40
+ // Normalise dedupe: true → { enabled: true } so the rest of the function
41
+ // always works with the object form.
42
+ const dedupe = typeof options.dedupe === 'boolean'
43
+ ? { enabled: options.dedupe }
44
+ : options.dedupe;
45
+ // Outermost -> innermost. See executor.ts for why this ordering matters.
46
+ const policies = [];
47
+ // totalTimeout sits before everything — it's a hard wall-clock budget over
48
+ // the entire operation. If it fires, no inner policy can extend the deadline.
49
+ if (options.totalTimeout && options.totalTimeout.ms > 0) {
50
+ policies.push((0, timeout_js_1.totalTimeoutPolicy)(options.totalTimeout)); // 0. hardest outer wall
51
+ }
52
+ if (options.cache && options.cache.ttl > 0) {
53
+ policies.push((0, cache_js_1.cachePolicy)(options.cache)); // 1. skip all on hit
54
+ }
55
+ if (dedupe?.enabled) {
56
+ policies.push((0, dedupe_js_1.dedupePolicy)()); // 2. collapse concurrent callers
57
+ }
58
+ if (options.retry && options.retry.attempts > 1) {
59
+ policies.push((0, retry_js_1.retryPolicy)(options.retry)); // 3. own the attempt loop
60
+ }
61
+ if (options.timeout && options.timeout.ms > 0) {
62
+ policies.push((0, timeout_js_1.timeoutPolicy)(options.timeout)); // 4. innermost — per-attempt clock
63
+ }
64
+ try {
65
+ const value = await (0, executor_js_1.execute)({ key, fn, policies, store: defaultStore, meta });
66
+ return { ok: true, value, source: meta.source, attempts: meta.attempts };
67
+ }
68
+ catch (error) {
69
+ return { ok: false, error, attempts: meta.attempts };
70
+ }
71
+ }
@@ -0,0 +1,27 @@
1
+ import type { ActFn, ActOptions, ActResult } from '../types/index.js';
2
+ /**
3
+ * Execute fn with the given reliability policies.
4
+ *
5
+ * @param key Stable identifier for this action. Scopes dedupe + cache.
6
+ * @param fn The async work to run.
7
+ * @param options Which policies to apply and how. All fields are optional.
8
+ *
9
+ * @returns ActResult<T> — always resolves, never throws.
10
+ * Check result.ok before reading result.value.
11
+ *
12
+ * @example
13
+ * const result = await act('user:42', () => fetchUser(42), {
14
+ * retry: { attempts: 3, delayMs: 200, backoff: 'exponential' },
15
+ * timeout: { ms: 5_000 },
16
+ * dedupe: true,
17
+ * cache: { ttl: 60_000 },
18
+ * })
19
+ *
20
+ * if (result.ok) {
21
+ * console.log(result.value, result.source, result.attempts)
22
+ * } else {
23
+ * console.error(result.error)
24
+ * }
25
+ */
26
+ export declare function act<T>(key: string, fn: ActFn<T>, options?: ActOptions): Promise<ActResult<T>>;
27
+ //# sourceMappingURL=act.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"act.d.ts","sourceRoot":"","sources":["../../src/core/act.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAA0B,MAAM,mBAAmB,CAAA;AAa7F;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,GAAG,CAAC,CAAC,EACzB,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,EACZ,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAwCvB"}
@@ -0,0 +1,69 @@
1
+ import { execute } from './executor.js';
2
+ import { retryPolicy } from '../policies/retry.js';
3
+ import { timeoutPolicy, totalTimeoutPolicy } from '../policies/timeout.js';
4
+ import { dedupePolicy } from '../policies/dedupe.js';
5
+ import { cachePolicy } from '../policies/cache.js';
6
+ import { InMemoryStore } from '../state/store.js';
7
+ // Module-level default store so cache and dedupe persist across calls.
8
+ // For SSR isolation or per-test control, construct an InMemoryStore and
9
+ // call execute() directly — it accepts any StateStore implementation.
10
+ const defaultStore = new InMemoryStore();
11
+ /**
12
+ * Execute fn with the given reliability policies.
13
+ *
14
+ * @param key Stable identifier for this action. Scopes dedupe + cache.
15
+ * @param fn The async work to run.
16
+ * @param options Which policies to apply and how. All fields are optional.
17
+ *
18
+ * @returns ActResult<T> — always resolves, never throws.
19
+ * Check result.ok before reading result.value.
20
+ *
21
+ * @example
22
+ * const result = await act('user:42', () => fetchUser(42), {
23
+ * retry: { attempts: 3, delayMs: 200, backoff: 'exponential' },
24
+ * timeout: { ms: 5_000 },
25
+ * dedupe: true,
26
+ * cache: { ttl: 60_000 },
27
+ * })
28
+ *
29
+ * if (result.ok) {
30
+ * console.log(result.value, result.source, result.attempts)
31
+ * } else {
32
+ * console.error(result.error)
33
+ * }
34
+ */
35
+ export async function act(key, fn, options = {}) {
36
+ const meta = { attempts: 1, source: 'fresh' };
37
+ // Normalise dedupe: true → { enabled: true } so the rest of the function
38
+ // always works with the object form.
39
+ const dedupe = typeof options.dedupe === 'boolean'
40
+ ? { enabled: options.dedupe }
41
+ : options.dedupe;
42
+ // Outermost -> innermost. See executor.ts for why this ordering matters.
43
+ const policies = [];
44
+ // totalTimeout sits before everything — it's a hard wall-clock budget over
45
+ // the entire operation. If it fires, no inner policy can extend the deadline.
46
+ if (options.totalTimeout && options.totalTimeout.ms > 0) {
47
+ policies.push(totalTimeoutPolicy(options.totalTimeout)); // 0. hardest outer wall
48
+ }
49
+ if (options.cache && options.cache.ttl > 0) {
50
+ policies.push(cachePolicy(options.cache)); // 1. skip all on hit
51
+ }
52
+ if (dedupe?.enabled) {
53
+ policies.push(dedupePolicy()); // 2. collapse concurrent callers
54
+ }
55
+ if (options.retry && options.retry.attempts > 1) {
56
+ policies.push(retryPolicy(options.retry)); // 3. own the attempt loop
57
+ }
58
+ if (options.timeout && options.timeout.ms > 0) {
59
+ policies.push(timeoutPolicy(options.timeout)); // 4. innermost — per-attempt clock
60
+ }
61
+ try {
62
+ const value = await execute({ key, fn, policies, store: defaultStore, meta });
63
+ return { ok: true, value, source: meta.source, attempts: meta.attempts };
64
+ }
65
+ catch (error) {
66
+ return { ok: false, error, attempts: meta.attempts };
67
+ }
68
+ }
69
+ //# sourceMappingURL=act.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"act.js","sourceRoot":"","sources":["../../src/core/act.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAa,eAAe,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAS,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAQ,uBAAuB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAS,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAO,mBAAmB,CAAA;AAElD,uEAAuE;AACvE,wEAAwE;AACxE,sEAAsE;AACtE,MAAM,YAAY,GAAG,IAAI,aAAa,EAAE,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,GAAG,CACvB,GAAW,EACX,EAAY,EACZ,UAAsB,EAAE;IAExB,MAAM,IAAI,GAAY,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IAEtD,yEAAyE;IACzE,qCAAqC;IACrC,MAAM,MAAM,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,SAAS;QAChD,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE;QAC7B,CAAC,CAAC,OAAO,CAAC,MAAM,CAAA;IAElB,yEAAyE;IACzE,MAAM,QAAQ,GAA4B,EAAE,CAAA;IAE5C,2EAA2E;IAC3E,8EAA8E;IAC9E,IAAI,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QACxD,QAAQ,CAAC,IAAI,CAAC,kBAAkB,CAAI,OAAO,CAAC,YAAY,CAAC,CAAC,CAAA,CAAC,wBAAwB;IACrF,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QAC3C,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA,CAAe,qBAAqB;IAClF,CAAC;IAED,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAK,CAAC,CAAA,CAA2B,iCAAiC;IAC9F,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA,CAAe,0BAA0B;IACvF,CAAC;IAED,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QAC9C,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA,CAAW,mCAAmC;IAChG,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAA;IAC1E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAA;IACtD,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execute = execute;
4
+ /**
5
+ * Pure execution engine.
6
+ *
7
+ * This file imports nothing from /policies.
8
+ * It operates on PolicyApplier<T> — a type alias defined in /types.
9
+ * Policy implementations live in /policies and are wired in core/act.ts.
10
+ */
11
+ async function execute(input) {
12
+ const ctx = {
13
+ key: input.key,
14
+ store: input.store,
15
+ meta: input.meta,
16
+ };
17
+ // Build the call chain from inside out.
18
+ // reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
19
+ const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
20
+ return wrapped();
21
+ }
@@ -0,0 +1,27 @@
1
+ import type { ActFn, PolicyApplier, StateStore, RunMeta } from '../types/index.js';
2
+ export interface ExecutorInput<T> {
3
+ key: string;
4
+ fn: ActFn<T>;
5
+ /**
6
+ * Policies ordered outermost -> innermost.
7
+ * policies[0] intercepts first; policies[last] is closest to fn.
8
+ *
9
+ * Canonical order: [cache, dedupe, retry, timeout]
10
+ * cache -> a hit skips everything below it
11
+ * dedupe -> collapses concurrent callers before retry fires
12
+ * retry -> owns the attempt loop
13
+ * timeout -> each individual attempt races against the clock
14
+ */
15
+ policies: ReadonlyArray<PolicyApplier<T>>;
16
+ store: StateStore;
17
+ meta: RunMeta;
18
+ }
19
+ /**
20
+ * Pure execution engine.
21
+ *
22
+ * This file imports nothing from /policies.
23
+ * It operates on PolicyApplier<T> — a type alias defined in /types.
24
+ * Policy implementations live in /policies and are wired in core/act.ts.
25
+ */
26
+ export declare function execute<T>(input: ExecutorInput<T>): Promise<T>;
27
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,aAAa,EAEb,UAAU,EACV,OAAO,EACR,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,GAAG,EAAO,MAAM,CAAA;IAChB,EAAE,EAAQ,KAAK,CAAC,CAAC,CAAC,CAAA;IAClB;;;;;;;;;OASG;IACH,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,KAAK,EAAK,UAAU,CAAA;IACpB,IAAI,EAAM,OAAO,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAepE"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pure execution engine.
3
+ *
4
+ * This file imports nothing from /policies.
5
+ * It operates on PolicyApplier<T> — a type alias defined in /types.
6
+ * Policy implementations live in /policies and are wired in core/act.ts.
7
+ */
8
+ export async function execute(input) {
9
+ const ctx = {
10
+ key: input.key,
11
+ store: input.store,
12
+ meta: input.meta,
13
+ };
14
+ // Build the call chain from inside out.
15
+ // reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
16
+ const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
17
+ return wrapped();
18
+ }
19
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AA0BA;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,KAAuB;IACtD,MAAM,GAAG,GAAkB;QACzB,GAAG,EAAI,KAAK,CAAC,GAAG;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAG,KAAK,CAAC,IAAI;KAClB,CAAA;IAED,wCAAwC;IACxC,8EAA8E;IAC9E,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,WAAW,CACxC,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,EAC/C,KAAK,CAAC,EAAE,CACT,CAAA;IAED,OAAO,OAAO,EAAE,CAAA;AAClB,CAAC"}
package/dist/index.cjs ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TotalTimeoutError = exports.TimeoutError = exports.InMemoryStore = exports.act = void 0;
4
+ var act_js_1 = require("./core/act.js");
5
+ Object.defineProperty(exports, "act", { enumerable: true, get: function () { return act_js_1.act; } });
6
+ // Exported so consumers can build isolated stores (e.g. per-request in SSR)
7
+ var store_js_1 = require("./state/store.js");
8
+ Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return store_js_1.InMemoryStore; } });
9
+ // Exported so callers can instanceof-check against timeout failures
10
+ var timeout_js_1 = require("./policies/timeout.js");
11
+ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return timeout_js_1.TimeoutError; } });
12
+ Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return timeout_js_1.TotalTimeoutError; } });
@@ -0,0 +1,5 @@
1
+ export { act } from './core/act.js';
2
+ export type { ActFn, ActResult, ActSuccess, ActFailure, ActSource, ActOptions, RetryOptions, TimeoutOptions, DedupeOptions, CacheOptions, StateStore, } from './types/index.js';
3
+ export { InMemoryStore } from './state/store.js';
4
+ export { TimeoutError, TotalTimeoutError } from './policies/timeout.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAEnC,YAAY,EACV,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAGhD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { act } from './core/act.js';
2
+ // Exported so consumers can build isolated stores (e.g. per-request in SSR)
3
+ export { InMemoryStore } from './state/store.js';
4
+ // Exported so callers can instanceof-check against timeout failures
5
+ export { TimeoutError, TotalTimeoutError } from './policies/timeout.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAgBnC,4EAA4E;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEhD,oEAAoE;AACpE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cachePolicy = cachePolicy;
4
+ const NS = 'cache:';
5
+ /**
6
+ * Short-circuits the entire downstream chain on a cache hit.
7
+ * On a miss, runs fn and stores the result with TTL.
8
+ *
9
+ * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
10
+ */
11
+ function cachePolicy(opts) {
12
+ return (fn, ctx) => async () => {
13
+ const key = NS + ctx.key;
14
+ const hit = ctx.store.get(key);
15
+ if (hit) {
16
+ ctx.meta.source = 'cache';
17
+ return hit.value;
18
+ }
19
+ const value = await fn();
20
+ ctx.store.set(key, { value }, opts.ttl);
21
+ return value;
22
+ };
23
+ }
@@ -0,0 +1,9 @@
1
+ import type { CacheOptions, PolicyApplier } from '../types/index.js';
2
+ /**
3
+ * Short-circuits the entire downstream chain on a cache hit.
4
+ * On a miss, runs fn and stores the result with TTL.
5
+ *
6
+ * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
7
+ */
8
+ export declare function cachePolicy<T>(opts: CacheOptions): PolicyApplier<T>;
9
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,YAAY,EAAE,aAAa,EAAiB,MAAM,mBAAmB,CAAA;AAS1F;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAenE"}
@@ -0,0 +1,21 @@
1
+ const NS = 'cache:';
2
+ /**
3
+ * Short-circuits the entire downstream chain on a cache hit.
4
+ * On a miss, runs fn and stores the result with TTL.
5
+ *
6
+ * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
7
+ */
8
+ export function cachePolicy(opts) {
9
+ return (fn, ctx) => async () => {
10
+ const key = NS + ctx.key;
11
+ const hit = ctx.store.get(key);
12
+ if (hit) {
13
+ ctx.meta.source = 'cache';
14
+ return hit.value;
15
+ }
16
+ const value = await fn();
17
+ ctx.store.set(key, { value }, opts.ttl);
18
+ return value;
19
+ };
20
+ }
21
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AAEA,MAAM,EAAE,GAAG,QAAQ,CAAA;AAOnB;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,CAAA;QAExB,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAA;QAC7C,IAAI,GAAG,EAAE,CAAC;YACR,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;YACzB,OAAO,GAAG,CAAC,KAAK,CAAA;QAClB,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAA;QACxB,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;QACtD,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;AACL,CAAC"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dedupePolicy = dedupePolicy;
4
+ // Namespace so dedupe keys never collide with cache keys in the shared store
5
+ const NS = 'dedupe:';
6
+ /**
7
+ * Collapses concurrent calls that share the same key into one in-flight Promise.
8
+ *
9
+ * The first caller starts the work. Every subsequent caller that arrives before
10
+ * the first resolves gets the same Promise back — no duplicate work.
11
+ *
12
+ * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
13
+ * the retry counter belongs to the originating call's meta object.
14
+ */
15
+ function dedupePolicy() {
16
+ return (fn, ctx) => async () => {
17
+ const key = NS + ctx.key;
18
+ const inflight = ctx.store.get(key);
19
+ if (inflight)
20
+ return inflight;
21
+ // No TTL — .finally() cleans up regardless of outcome
22
+ const promise = fn().finally(() => ctx.store.delete(key));
23
+ ctx.store.set(key, promise);
24
+ return promise;
25
+ };
26
+ }
@@ -0,0 +1,12 @@
1
+ import type { PolicyApplier } from '../types/index.js';
2
+ /**
3
+ * Collapses concurrent calls that share the same key into one in-flight Promise.
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.
7
+ *
8
+ * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
9
+ * the retry counter belongs to the originating call's meta object.
10
+ */
11
+ export declare function dedupePolicy<T>(): PolicyApplier<T>;
12
+ //# sourceMappingURL=dedupe.d.ts.map
@@ -0,0 +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;AAK5E;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAalD"}
@@ -0,0 +1,24 @@
1
+ // Namespace so dedupe keys never collide with cache keys in the shared store
2
+ const NS = 'dedupe:';
3
+ /**
4
+ * Collapses concurrent calls that share the same key into one in-flight Promise.
5
+ *
6
+ * The first caller starts the work. Every subsequent caller that arrives before
7
+ * the first resolves gets the same Promise back — no duplicate work.
8
+ *
9
+ * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
10
+ * the retry counter belongs to the originating call's meta object.
11
+ */
12
+ export function dedupePolicy() {
13
+ return (fn, ctx) => async () => {
14
+ const key = NS + ctx.key;
15
+ const inflight = ctx.store.get(key);
16
+ if (inflight)
17
+ return inflight;
18
+ // No TTL — .finally() cleans up regardless of outcome
19
+ const promise = fn().finally(() => ctx.store.delete(key));
20
+ ctx.store.set(key, promise);
21
+ return promise;
22
+ };
23
+ }
24
+ //# sourceMappingURL=dedupe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,MAAM,EAAE,GAAG,SAAS,CAAA;AAEpB;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,CAAA;QAExB,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,CAAC,CAAA;QAC/C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAE7B,sDAAsD;QACtD,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QACzD,GAAG,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,EAAE,OAAO,CAAC,CAAA;QACvC,OAAO,OAAO,CAAA;IAChB,CAAC,CAAA;AACL,CAAC"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.retryPolicy = retryPolicy;
4
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
5
+ function computeDelay(attempt, opts) {
6
+ const base = opts.delayMs ?? 0;
7
+ if (base === 0)
8
+ return 0;
9
+ switch (opts.backoff ?? 'none') {
10
+ case 'linear': return base * attempt;
11
+ case 'exponential': return base * 2 ** (attempt - 1);
12
+ default: return base;
13
+ }
14
+ }
15
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
16
+ // ─── Policy ──────────────────────────────────────────────────────────────────
17
+ /**
18
+ * Retries fn up to opts.attempts times on any thrown error.
19
+ * Writes the live attempt count into ctx.meta.attempts.
20
+ */
21
+ function retryPolicy(opts) {
22
+ const max = Math.max(1, opts.attempts);
23
+ return (fn, ctx) => async () => {
24
+ let lastErr;
25
+ for (let attempt = 1; attempt <= max; attempt++) {
26
+ ctx.meta.attempts = attempt;
27
+ try {
28
+ return await fn();
29
+ }
30
+ catch (err) {
31
+ lastErr = err;
32
+ if (attempt < max) {
33
+ // Non-retryable error — bail immediately without consuming
34
+ // remaining attempts. The error surfaces exactly as-is.
35
+ if (opts.shouldRetry && !opts.shouldRetry(err, attempt)) {
36
+ throw err;
37
+ }
38
+ const delay = computeDelay(attempt, opts);
39
+ if (delay > 0)
40
+ await sleep(delay);
41
+ }
42
+ }
43
+ }
44
+ // Every attempt failed — surface the last error upstream
45
+ throw lastErr;
46
+ };
47
+ }
@@ -0,0 +1,7 @@
1
+ import type { PolicyApplier, RetryOptions } from '../types/index.js';
2
+ /**
3
+ * Retries fn up to opts.attempts times on any thrown error.
4
+ * Writes the live attempt count into ctx.meta.attempts.
5
+ */
6
+ export declare function retryPolicy<T>(opts: RetryOptions): PolicyApplier<T>;
7
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAmB1F;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CA8BnE"}
@@ -0,0 +1,45 @@
1
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
2
+ function computeDelay(attempt, opts) {
3
+ const base = opts.delayMs ?? 0;
4
+ if (base === 0)
5
+ return 0;
6
+ switch (opts.backoff ?? 'none') {
7
+ case 'linear': return base * attempt;
8
+ case 'exponential': return base * 2 ** (attempt - 1);
9
+ default: return base;
10
+ }
11
+ }
12
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
13
+ // ─── Policy ──────────────────────────────────────────────────────────────────
14
+ /**
15
+ * Retries fn up to opts.attempts times on any thrown error.
16
+ * Writes the live attempt count into ctx.meta.attempts.
17
+ */
18
+ export function retryPolicy(opts) {
19
+ const max = Math.max(1, opts.attempts);
20
+ return (fn, ctx) => async () => {
21
+ let lastErr;
22
+ for (let attempt = 1; attempt <= max; attempt++) {
23
+ ctx.meta.attempts = attempt;
24
+ try {
25
+ return await fn();
26
+ }
27
+ catch (err) {
28
+ lastErr = err;
29
+ if (attempt < max) {
30
+ // Non-retryable error — bail immediately without consuming
31
+ // remaining attempts. The error surfaces exactly as-is.
32
+ if (opts.shouldRetry && !opts.shouldRetry(err, attempt)) {
33
+ throw err;
34
+ }
35
+ const delay = computeDelay(attempt, opts);
36
+ if (delay > 0)
37
+ await sleep(delay);
38
+ }
39
+ }
40
+ }
41
+ // Every attempt failed — surface the last error upstream
42
+ throw lastErr;
43
+ };
44
+ }
45
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAEhF,SAAS,YAAY,CAAC,OAAe,EAAE,IAAkB;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAExB,QAAQ,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,CAAC;QAC/B,KAAK,QAAQ,CAAC,CAAM,OAAO,IAAI,GAAG,OAAO,CAAA;QACzC,KAAK,aAAa,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;QACpD,OAAO,CAAC,CAAY,OAAO,IAAI,CAAA;IACjC,CAAC;AACH,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AAEvE,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IAEtC,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,IAAI,OAAgB,CAAA;QAEpB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;YAChD,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;YAC3B,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAA;YACnB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,GAAG,CAAA;gBAEb,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;oBAClB,2DAA2D;oBAC3D,wDAAwD;oBACxD,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;wBACxD,MAAM,GAAG,CAAA;oBACX,CAAC;oBAED,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;oBACzC,IAAI,KAAK,GAAG,CAAC;wBAAE,MAAM,KAAK,CAAC,KAAK,CAAC,CAAA;gBACnC,CAAC;YACH,CAAC;QACH,CAAC;QAED,yDAAyD;QACzD,MAAM,OAAO,CAAA;IACf,CAAC,CAAA;AACL,CAAC"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TotalTimeoutError = exports.TimeoutError = void 0;
4
+ exports.timeoutPolicy = timeoutPolicy;
5
+ exports.totalTimeoutPolicy = totalTimeoutPolicy;
6
+ // ─── Errors ───────────────────────────────────────────────────────────────────
7
+ class TimeoutError extends Error {
8
+ constructor(ms) {
9
+ super(`ACT timed out after ${ms}ms`);
10
+ this.name = 'TimeoutError';
11
+ this.ms = ms;
12
+ }
13
+ }
14
+ exports.TimeoutError = TimeoutError;
15
+ /**
16
+ * Thrown when the total budget across all attempts is exceeded.
17
+ * Distinct from TimeoutError (per-attempt) so callers can instanceof-check
18
+ * which deadline fired.
19
+ */
20
+ class TotalTimeoutError extends Error {
21
+ constructor(ms) {
22
+ super(`ACT total timeout exceeded after ${ms}ms`);
23
+ this.name = 'TotalTimeoutError';
24
+ this.ms = ms;
25
+ }
26
+ }
27
+ exports.TotalTimeoutError = TotalTimeoutError;
28
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
29
+ /** Shared race-against-a-timer logic. ErrorCtor lets callers pick the error type. */
30
+ function makeTimeoutPolicy(opts, ErrorCtor) {
31
+ return (fn, _ctx) => () => new Promise((resolve, reject) => {
32
+ const timer = setTimeout(() => reject(new ErrorCtor(opts.ms)), opts.ms);
33
+ fn()
34
+ .then((v) => { clearTimeout(timer); resolve(v); })
35
+ .catch((e) => { clearTimeout(timer); reject(e); });
36
+ });
37
+ }
38
+ // ─── Policies ─────────────────────────────────────────────────────────────────
39
+ /**
40
+ * Races fn against a hard deadline.
41
+ * Rejects with TimeoutError if the deadline fires first.
42
+ *
43
+ * Place this INSIDE retryPolicy (closer to fn) so each attempt has its own clock.
44
+ */
45
+ function timeoutPolicy(opts) {
46
+ return makeTimeoutPolicy(opts, TimeoutError);
47
+ }
48
+ /**
49
+ * Races the ENTIRE operation (all retry attempts + delays) against a budget.
50
+ * Rejects with TotalTimeoutError if the budget is exhausted.
51
+ *
52
+ * Place this as the OUTERMOST policy so the clock starts before any other
53
+ * policy runs and stops regardless of what the inner chain is doing.
54
+ */
55
+ function totalTimeoutPolicy(opts) {
56
+ return makeTimeoutPolicy(opts, TotalTimeoutError);
57
+ }
@@ -0,0 +1,30 @@
1
+ import type { PolicyApplier, TimeoutOptions } from '../types/index.js';
2
+ export declare class TimeoutError extends Error {
3
+ readonly ms: number;
4
+ constructor(ms: number);
5
+ }
6
+ /**
7
+ * Thrown when the total budget across all attempts is exceeded.
8
+ * Distinct from TimeoutError (per-attempt) so callers can instanceof-check
9
+ * which deadline fired.
10
+ */
11
+ export declare class TotalTimeoutError extends Error {
12
+ readonly ms: number;
13
+ constructor(ms: number);
14
+ }
15
+ /**
16
+ * Races fn against a hard deadline.
17
+ * Rejects with TimeoutError if the deadline fires first.
18
+ *
19
+ * Place this INSIDE retryPolicy (closer to fn) so each attempt has its own clock.
20
+ */
21
+ export declare function timeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
22
+ /**
23
+ * Races the ENTIRE operation (all retry attempts + delays) against a budget.
24
+ * Rejects with TotalTimeoutError if the budget is exhausted.
25
+ *
26
+ * Place this as the OUTERMOST policy so the clock starts before any other
27
+ * policy runs and stops regardless of what the inner chain is doing.
28
+ */
29
+ export declare function totalTimeoutPolicy<T>(opts: TimeoutOptions): PolicyApplier<T>;
30
+ //# sourceMappingURL=timeout.d.ts.map
@@ -0,0 +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;AAI5F,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;gBAEP,EAAE,EAAE,MAAM;CAKvB;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;gBAEP,EAAE,EAAE,MAAM;CAKvB;AAyBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAE5E"}
@@ -0,0 +1,51 @@
1
+ // ─── Errors ───────────────────────────────────────────────────────────────────
2
+ export class TimeoutError extends Error {
3
+ constructor(ms) {
4
+ super(`ACT timed out after ${ms}ms`);
5
+ this.name = 'TimeoutError';
6
+ this.ms = ms;
7
+ }
8
+ }
9
+ /**
10
+ * Thrown when the total budget across all attempts is exceeded.
11
+ * Distinct from TimeoutError (per-attempt) so callers can instanceof-check
12
+ * which deadline fired.
13
+ */
14
+ export class TotalTimeoutError extends Error {
15
+ constructor(ms) {
16
+ super(`ACT total timeout exceeded after ${ms}ms`);
17
+ this.name = 'TotalTimeoutError';
18
+ this.ms = ms;
19
+ }
20
+ }
21
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
22
+ /** Shared race-against-a-timer logic. ErrorCtor lets callers pick the error type. */
23
+ function makeTimeoutPolicy(opts, ErrorCtor) {
24
+ return (fn, _ctx) => () => new Promise((resolve, reject) => {
25
+ const timer = setTimeout(() => reject(new ErrorCtor(opts.ms)), opts.ms);
26
+ fn()
27
+ .then((v) => { clearTimeout(timer); resolve(v); })
28
+ .catch((e) => { clearTimeout(timer); reject(e); });
29
+ });
30
+ }
31
+ // ─── Policies ─────────────────────────────────────────────────────────────────
32
+ /**
33
+ * Races fn against a hard deadline.
34
+ * Rejects with TimeoutError if the deadline fires first.
35
+ *
36
+ * Place this INSIDE retryPolicy (closer to fn) so each attempt has its own clock.
37
+ */
38
+ export function timeoutPolicy(opts) {
39
+ return makeTimeoutPolicy(opts, TimeoutError);
40
+ }
41
+ /**
42
+ * Races the ENTIRE operation (all retry attempts + delays) against a budget.
43
+ * Rejects with TotalTimeoutError if the budget is exhausted.
44
+ *
45
+ * Place this as the OUTERMOST policy so the clock starts before any other
46
+ * policy runs and stops regardless of what the inner chain is doing.
47
+ */
48
+ export function totalTimeoutPolicy(opts) {
49
+ return makeTimeoutPolicy(opts, TotalTimeoutError);
50
+ }
51
+ //# sourceMappingURL=timeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/policies/timeout.ts"],"names":[],"mappings":"AAEA,iFAAiF;AAEjF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAGrC,YAAY,EAAU;QACpB,KAAK,CAAC,uBAAuB,EAAE,IAAI,CAAC,CAAA;QACpC,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;QAC1B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAG1C,YAAY,EAAU;QACpB,KAAK,CAAC,oCAAoC,EAAE,IAAI,CAAC,CAAA;QACjD,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;QAC/B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;CACF;AAED,iFAAiF;AAEjF,qFAAqF;AACrF,SAAS,iBAAiB,CACxB,IAAoB,EACpB,SAAoC;IAEpC,OAAO,CAAC,EAAY,EAAE,IAAmB,EAAY,EAAE,CACrD,GAAG,EAAE,CACH,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACjC,MAAM,KAAK,GAAG,UAAU,CACtB,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EACpC,IAAI,CAAC,EAAE,CACR,CAAA;QAED,EAAE,EAAE;aACD,IAAI,CAAC,CAAC,CAAI,EAAS,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAC;aAC1D,KAAK,CAAC,CAAC,CAAU,EAAE,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA,CAAE,CAAC,CAAC,CAAA;IAC/D,CAAC,CAAC,CAAA;AACR,CAAC;AAED,iFAAiF;AAEjF;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAI,IAAoB;IACnD,OAAO,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAI,IAAoB;IACxD,OAAO,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;AACnD,CAAC"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryStore = void 0;
4
+ class InMemoryStore {
5
+ constructor() {
6
+ this.entries = new Map();
7
+ }
8
+ get(key) {
9
+ const entry = this.entries.get(key);
10
+ if (!entry)
11
+ return undefined;
12
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
13
+ this.entries.delete(key);
14
+ return undefined;
15
+ }
16
+ return entry.value;
17
+ }
18
+ set(key, value, ttlMs) {
19
+ // ttlMs = 0 or undefined -> no expiry (sentinel null)
20
+ const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
21
+ this.entries.set(key, { value, expiresAt });
22
+ }
23
+ delete(key) {
24
+ this.entries.delete(key);
25
+ }
26
+ has(key) {
27
+ // Reuse get() so expired entries are evicted on access
28
+ return this.get(key) !== undefined;
29
+ }
30
+ /** Drop everything. Handy in tests or for manual cache invalidation. */
31
+ clear() {
32
+ this.entries.clear();
33
+ }
34
+ /** Count of live (non-expired) entries. */
35
+ get size() {
36
+ for (const key of this.entries.keys())
37
+ this.has(key);
38
+ return this.entries.size;
39
+ }
40
+ }
41
+ exports.InMemoryStore = InMemoryStore;
@@ -0,0 +1,13 @@
1
+ import type { StateStore } from '../types/index.js';
2
+ export declare class InMemoryStore implements StateStore {
3
+ private entries;
4
+ get<T>(key: string): T | undefined;
5
+ set<T>(key: string, value: T, ttlMs?: number): void;
6
+ delete(key: string): void;
7
+ has(key: string): boolean;
8
+ /** Drop everything. Handy in tests or for manual cache invalidation. */
9
+ clear(): void;
10
+ /** Count of live (non-expired) entries. */
11
+ get size(): number;
12
+ }
13
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAQnD,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,OAAO,CAAoC;IAEnD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAYlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAOnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAKzB,wEAAwE;IACxE,KAAK,IAAI,IAAI;IAIb,2CAA2C;IAC3C,IAAI,IAAI,IAAI,MAAM,CAGjB;CACF"}
@@ -0,0 +1,38 @@
1
+ export class InMemoryStore {
2
+ constructor() {
3
+ this.entries = new Map();
4
+ }
5
+ get(key) {
6
+ const entry = this.entries.get(key);
7
+ if (!entry)
8
+ return undefined;
9
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
10
+ this.entries.delete(key);
11
+ return undefined;
12
+ }
13
+ return entry.value;
14
+ }
15
+ set(key, value, ttlMs) {
16
+ // ttlMs = 0 or undefined -> no expiry (sentinel null)
17
+ const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
18
+ this.entries.set(key, { value, expiresAt });
19
+ }
20
+ delete(key) {
21
+ this.entries.delete(key);
22
+ }
23
+ has(key) {
24
+ // Reuse get() so expired entries are evicted on access
25
+ return this.get(key) !== undefined;
26
+ }
27
+ /** Drop everything. Handy in tests or for manual cache invalidation. */
28
+ clear() {
29
+ this.entries.clear();
30
+ }
31
+ /** Count of live (non-expired) entries. */
32
+ get size() {
33
+ for (const key of this.entries.keys())
34
+ this.has(key);
35
+ return this.entries.size;
36
+ }
37
+ }
38
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,aAAa;IAA1B;QACU,YAAO,GAAG,IAAI,GAAG,EAA0B,CAAA;IAwCrD,CAAC;IAtCC,GAAG,CAAI,GAAW;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAA;QAE5B,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YAC7D,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACxB,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,OAAO,KAAK,CAAC,KAAU,CAAA;IACzB,CAAC;IAED,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,KAAc;QAC1C,sDAAsD;QACtD,MAAM,SAAS,GACb,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;QACxD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,GAAG,CAAC,GAAW;QACb,uDAAuD;QACvD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,CAAA;IACpC,CAAC;IAED,wEAAwE;IACxE,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;IACtB,CAAC;IAED,2CAA2C;IAC3C,IAAI,IAAI;QACN,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACpD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // ─── Public surface ────────────────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,116 @@
1
+ /** The async function ACT wraps */
2
+ export type ActFn<T> = () => Promise<T>;
3
+ /** Where a successful result came from */
4
+ export type ActSource = 'fresh' | 'cache';
5
+ export interface ActSuccess<T> {
6
+ ok: true;
7
+ value: T;
8
+ source: ActSource;
9
+ /** Number of attempts made before success */
10
+ attempts: number;
11
+ }
12
+ export interface ActFailure {
13
+ ok: false;
14
+ error: unknown;
15
+ /** Number of attempts made before final failure */
16
+ attempts: number;
17
+ }
18
+ export type ActResult<T> = ActSuccess<T> | ActFailure;
19
+ export interface RetryOptions {
20
+ /** Total number of attempts including the first call. Must be >= 1. */
21
+ attempts: number;
22
+ /** Base delay between attempts in ms. 0 = no delay. */
23
+ delayMs?: number;
24
+ /**
25
+ * How the base delay grows per attempt:
26
+ * - 'none' -> always delayMs
27
+ * - 'linear' -> delayMs * attempt
28
+ * - 'exponential' -> delayMs * 2^(attempt-1)
29
+ */
30
+ backoff?: 'none' | 'linear' | 'exponential';
31
+ /**
32
+ * Predicate called after each failure, before the next attempt.
33
+ * Return false to stop retrying immediately and surface the error.
34
+ *
35
+ * Use this to skip retries for errors that are definitively non-recoverable
36
+ * (e.g. HTTP 4xx, AuthError, ValidationError).
37
+ *
38
+ * @param error The error thrown by the most recent attempt.
39
+ * @param attempt The 1-based number of the attempt that just failed.
40
+ *
41
+ * @example
42
+ * shouldRetry: (err, attempt) =>
43
+ * !(err instanceof HttpError && err.status < 500)
44
+ */
45
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
46
+ }
47
+ export interface TimeoutOptions {
48
+ /** Abort after this many ms */
49
+ ms: number;
50
+ }
51
+ export interface DedupeOptions {
52
+ /**
53
+ * Collapse concurrent calls sharing the same key into one in-flight Promise.
54
+ * Opt-in: be explicit when you want this behaviour.
55
+ */
56
+ enabled: boolean;
57
+ }
58
+ export interface CacheOptions {
59
+ /** Keep a successful result for this many ms */
60
+ ttl: number;
61
+ }
62
+ export interface ActOptions {
63
+ retry?: RetryOptions;
64
+ timeout?: TimeoutOptions;
65
+ /**
66
+ * Collapse concurrent calls with the same key into one in-flight Promise.
67
+ *
68
+ * Shorthand: `dedupe: true`
69
+ * Full form: `dedupe: { enabled: true }`
70
+ *
71
+ * Both are equivalent. The object form exists for forward compatibility.
72
+ */
73
+ dedupe?: boolean | DedupeOptions;
74
+ cache?: CacheOptions;
75
+ /**
76
+ * Hard budget over the ENTIRE operation — including all retry attempts,
77
+ * delays, and the timeout per attempt.
78
+ *
79
+ * Distinct from `timeout`, which resets the clock on every attempt.
80
+ * Use both together to express: "each attempt may take at most X ms,
81
+ * but the whole thing must finish within Y ms."
82
+ *
83
+ * Rejects with TotalTimeoutError if the deadline fires.
84
+ */
85
+ totalTimeout?: TimeoutOptions;
86
+ }
87
+ /**
88
+ * Mutable bag mutated in-place during execution.
89
+ * Policies annotate it; act() reads the final state to build ActResult.
90
+ */
91
+ export interface RunMeta {
92
+ attempts: number;
93
+ source: ActSource;
94
+ }
95
+ /** Everything a policy receives about the current run */
96
+ export interface PolicyContext {
97
+ key: string;
98
+ store: StateStore;
99
+ meta: RunMeta;
100
+ }
101
+ /**
102
+ * The ONLY shape the executor knows about policies.
103
+ *
104
+ * A policy wraps ActFn<T> and returns a new ActFn<T>.
105
+ * It may intercept before, after, or instead of the inner call.
106
+ * The executor never imports a concrete policy — only this type.
107
+ */
108
+ export type PolicyApplier<T> = (fn: ActFn<T>, ctx: PolicyContext) => ActFn<T>;
109
+ /** Minimal key-value contract used by dedupe and cache policies */
110
+ export interface StateStore {
111
+ get<T>(key: string): T | undefined;
112
+ set<T>(key: string, value: T, ttlMs?: number): void;
113
+ delete(key: string): void;
114
+ has(key: string): boolean;
115
+ }
116
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,mCAAmC;AACnC,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CAAA;AAEvC,0CAA0C;AAC1C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEzC,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,EAAE,EAAE,IAAI,CAAA;IACR,KAAK,EAAE,CAAC,CAAA;IACR,MAAM,EAAE,SAAS,CAAA;IACjB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,KAAK,CAAA;IACT,KAAK,EAAE,OAAO,CAAA;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAA;AAIrD,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,aAAa,CAAA;IAC3C;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;CAC3D;AAED,MAAM,WAAW,cAAc;IAC7B,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAA;CACX;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAS,YAAY,CAAA;IAC3B,OAAO,CAAC,EAAO,cAAc,CAAA;IAC7B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAQ,OAAO,GAAG,aAAa,CAAA;IACtC,KAAK,CAAC,EAAS,YAAY,CAAA;IAC3B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,cAAc,CAAA;CAC9B;AAID;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAI,SAAS,CAAA;CACpB;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAI,MAAM,CAAA;IACb,KAAK,EAAE,UAAU,CAAA;IACjB,IAAI,EAAG,OAAO,CAAA;CACf;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,aAAa,KAAK,KAAK,CAAC,CAAC,CAAC,CAAA;AAI7E,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,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;CAC1B"}
@@ -0,0 +1,3 @@
1
+ // ─── Public surface ────────────────────────────────────────────────────────────
2
+ export {};
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,kFAAkF"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "actly",
3
+ "version": "1.0.0",
4
+ "description": "A reliability primitive for executing async actions safely.",
5
+ "keywords": [
6
+ "reliability",
7
+ "retry",
8
+ "timeout",
9
+ "dedupe",
10
+ "cache",
11
+ "async"
12
+ ],
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs",
22
+ "types": "./dist/index.d.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc --project tsconfig.build.json && tsc --project tsconfig.cjs.json && node scripts/postbuild.mjs",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "jest"
34
+ },
35
+ "jest": {
36
+ "preset": "ts-jest/presets/default-esm",
37
+ "testEnvironment": "node",
38
+ "extensionsToTreatAsEsm": [
39
+ ".ts"
40
+ ],
41
+ "moduleNameMapper": {
42
+ "^(\\.{1,2}/.*)\\.js$": "$1"
43
+ },
44
+ "transform": {
45
+ "^.+\\.tsx?$": [
46
+ "ts-jest",
47
+ {
48
+ "useESM": true,
49
+ "tsconfig": {
50
+ "module": "ESNext",
51
+ "moduleResolution": "bundler"
52
+ }
53
+ }
54
+ ]
55
+ },
56
+ "testMatch": [
57
+ "**/src/__tests__/**/*.test.ts"
58
+ ],
59
+ "clearMocks": true,
60
+ "restoreMocks": true
61
+ },
62
+ "devDependencies": {
63
+ "@types/jest": "^29.5.0",
64
+ "jest": "^29.7.0",
65
+ "ts-jest": "^29.4.0",
66
+ "typescript": "^5.4.0"
67
+ }
68
+ }