actly 1.1.0 → 1.1.5
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 +284 -52
- package/dist/core/act.cjs +183 -35
- package/dist/core/act.d.ts +81 -10
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +181 -35
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +19 -9
- package/dist/core/executor.d.ts +25 -5
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +19 -9
- package/dist/core/executor.js.map +1 -1
- package/dist/index.cjs +13 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/policies/cache.cjs +85 -21
- package/dist/policies/cache.d.ts +29 -7
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +85 -21
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/dedupe.cjs +68 -20
- package/dist/policies/dedupe.d.ts +38 -13
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +68 -20
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/retry.cjs +65 -25
- package/dist/policies/retry.d.ts +25 -2
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +65 -25
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +87 -17
- package/dist/policies/timeout.d.ts +28 -8
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +87 -17
- 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 +77 -28
- package/dist/stores/memory.d.ts +44 -23
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +77 -28
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +141 -36
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +142 -0
- package/dist/utils/abort.d.ts +55 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +136 -0
- package/dist/utils/abort.js.map +1 -0
- package/dist/utils/backoff.cjs +43 -0
- package/dist/utils/backoff.d.ts +14 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +41 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/validate.cjs +79 -0
- package/dist/utils/validate.d.ts +15 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +72 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +19 -37
package/dist/core/executor.cjs
CHANGED
|
@@ -3,22 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.REQUIRES_SYNC_STORE = void 0;
|
|
4
4
|
exports.execute = execute;
|
|
5
5
|
const base_js_1 = require("../stores/base.js");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Symbol stamped onto `PolicyApplier` functions by `dedupePolicy`.
|
|
8
|
+
*
|
|
9
|
+
* Lets `execute()` detect a dedupe policy without importing the policy
|
|
10
|
+
* module (which would create a circular dep) or doing fragile name-sniffing.
|
|
11
|
+
*/
|
|
9
12
|
exports.REQUIRES_SYNC_STORE = Symbol('actly.requiresSyncStore');
|
|
10
13
|
/**
|
|
11
14
|
* Pure execution engine.
|
|
12
15
|
*
|
|
13
|
-
* This file imports nothing from
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
17
|
+
* — a type alias defined in `/types`. Policy implementations live in
|
|
18
|
+
* `/policies` and are wired in `core/act.ts`.
|
|
19
|
+
*
|
|
20
|
+
* # Public API
|
|
21
|
+
*
|
|
22
|
+
* Exported so consumers can build custom policy chains with explicit stores
|
|
23
|
+
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
24
|
+
* module-level default store is wrong).
|
|
16
25
|
*/
|
|
17
26
|
async function execute(input) {
|
|
18
27
|
// Guard: if any policy in the chain requires a sync store, the provided
|
|
19
28
|
// store must be synchronous. An async store + dedupePolicy is a silent
|
|
20
|
-
// correctness failure
|
|
21
|
-
//
|
|
29
|
+
// correctness failure — catch it here rather than letting it produce
|
|
30
|
+
// subtly wrong dedupe behaviour at runtime.
|
|
22
31
|
const needsSync = input.policies.some(p => p[exports.REQUIRES_SYNC_STORE]);
|
|
23
32
|
if (needsSync && !(0, base_js_1.isSyncStore)(input.store)) {
|
|
24
33
|
throw new Error('Actly: dedupePolicy requires a SyncStateStore (store._sync === true). ' +
|
|
@@ -33,5 +42,6 @@ async function execute(input) {
|
|
|
33
42
|
// Build the call chain from inside out.
|
|
34
43
|
// reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
|
|
35
44
|
const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
|
|
36
|
-
|
|
45
|
+
// Call the outermost wrapper with the root signal.
|
|
46
|
+
return wrapped(input.signal);
|
|
37
47
|
}
|
package/dist/core/executor.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { ActFn, PolicyApplier, AnyStateStore, RunMeta } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol stamped onto `PolicyApplier` functions by `dedupePolicy`.
|
|
4
|
+
*
|
|
5
|
+
* Lets `execute()` detect a dedupe policy without importing the policy
|
|
6
|
+
* module (which would create a circular dep) or doing fragile name-sniffing.
|
|
7
|
+
*/
|
|
2
8
|
export declare const REQUIRES_SYNC_STORE: unique symbol;
|
|
3
9
|
export interface ExecutorInput<T> {
|
|
4
10
|
key: string;
|
|
5
11
|
fn: ActFn<T>;
|
|
6
12
|
/**
|
|
7
13
|
* Policies ordered outermost -> innermost.
|
|
8
|
-
* policies[0] intercepts first; policies[last] is closest to fn
|
|
14
|
+
* `policies[0]` intercepts first; `policies[last]` is closest to `fn`.
|
|
9
15
|
*
|
|
10
|
-
* Canonical order: [totalTimeout, cache, dedupe, retry, timeout]
|
|
16
|
+
* Canonical order: `[totalTimeout, cache, dedupe, retry, timeout]`
|
|
11
17
|
* totalTimeout -> hard wall-clock budget over the entire operation
|
|
12
18
|
* cache -> a hit skips everything below it
|
|
13
19
|
* dedupe -> collapses concurrent callers before retry fires
|
|
@@ -17,13 +23,27 @@ export interface ExecutorInput<T> {
|
|
|
17
23
|
policies: ReadonlyArray<PolicyApplier<T>>;
|
|
18
24
|
store: AnyStateStore;
|
|
19
25
|
meta: RunMeta;
|
|
26
|
+
/**
|
|
27
|
+
* Root AbortSignal for the operation. Propagated inward through the
|
|
28
|
+
* policy chain: each policy receives it as the `signal` argument to
|
|
29
|
+
* its wrapped `ActFn`. The outermost policy may layer its own signal
|
|
30
|
+
* (e.g. `totalTimeoutPolicy` arms a timer) and pass the composite
|
|
31
|
+
* inward.
|
|
32
|
+
*/
|
|
33
|
+
signal: AbortSignal;
|
|
20
34
|
}
|
|
21
35
|
/**
|
|
22
36
|
* Pure execution engine.
|
|
23
37
|
*
|
|
24
|
-
* This file imports nothing from
|
|
25
|
-
*
|
|
26
|
-
*
|
|
38
|
+
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
39
|
+
* — a type alias defined in `/types`. Policy implementations live in
|
|
40
|
+
* `/policies` and are wired in `core/act.ts`.
|
|
41
|
+
*
|
|
42
|
+
* # Public API
|
|
43
|
+
*
|
|
44
|
+
* Exported so consumers can build custom policy chains with explicit stores
|
|
45
|
+
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
46
|
+
* module-level default store is wrong).
|
|
27
47
|
*/
|
|
28
48
|
export declare function execute<T>(input: ExecutorInput<T>): Promise<T>;
|
|
29
49
|
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,aAAa,EAEb,aAAa,EACb,OAAO,EACR,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,aAAa,EAEb,aAAa,EACb,OAAO,EACR,MAAM,mBAAmB,CAAA;AAG1B;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,eAAoC,CAAA;AAEpE,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,GAAG,EAAO,MAAM,CAAA;IAChB,EAAE,EAAQ,KAAK,CAAC,CAAC,CAAC,CAAA;IAClB;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,KAAK,EAAK,aAAa,CAAA;IACvB,IAAI,EAAM,OAAO,CAAA;IACjB;;;;;;OAMG;IACH,MAAM,EAAI,WAAW,CAAA;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA+BpE"}
|
package/dist/core/executor.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { isSyncStore } from '../stores/base.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Symbol stamped onto `PolicyApplier` functions by `dedupePolicy`.
|
|
4
|
+
*
|
|
5
|
+
* Lets `execute()` detect a dedupe policy without importing the policy
|
|
6
|
+
* module (which would create a circular dep) or doing fragile name-sniffing.
|
|
7
|
+
*/
|
|
5
8
|
export const REQUIRES_SYNC_STORE = Symbol('actly.requiresSyncStore');
|
|
6
9
|
/**
|
|
7
10
|
* Pure execution engine.
|
|
8
11
|
*
|
|
9
|
-
* This file imports nothing from
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* This file imports nothing from `/policies`. It operates on `PolicyApplier<T>`
|
|
13
|
+
* — a type alias defined in `/types`. Policy implementations live in
|
|
14
|
+
* `/policies` and are wired in `core/act.ts`.
|
|
15
|
+
*
|
|
16
|
+
* # Public API
|
|
17
|
+
*
|
|
18
|
+
* Exported so consumers can build custom policy chains with explicit stores
|
|
19
|
+
* (e.g. for SSR request isolation or multi-tenant scenarios where the
|
|
20
|
+
* module-level default store is wrong).
|
|
12
21
|
*/
|
|
13
22
|
export async function execute(input) {
|
|
14
23
|
// Guard: if any policy in the chain requires a sync store, the provided
|
|
15
24
|
// store must be synchronous. An async store + dedupePolicy is a silent
|
|
16
|
-
// correctness failure
|
|
17
|
-
//
|
|
25
|
+
// correctness failure — catch it here rather than letting it produce
|
|
26
|
+
// subtly wrong dedupe behaviour at runtime.
|
|
18
27
|
const needsSync = input.policies.some(p => p[REQUIRES_SYNC_STORE]);
|
|
19
28
|
if (needsSync && !isSyncStore(input.store)) {
|
|
20
29
|
throw new Error('Actly: dedupePolicy requires a SyncStateStore (store._sync === true). ' +
|
|
@@ -29,6 +38,7 @@ export async function execute(input) {
|
|
|
29
38
|
// Build the call chain from inside out.
|
|
30
39
|
// reduceRight ensures policies[0] becomes the outermost wrapper (runs first).
|
|
31
40
|
const wrapped = input.policies.reduceRight((inner, applyPolicy) => applyPolicy(inner, ctx), input.fn);
|
|
32
|
-
|
|
41
|
+
// Call the outermost wrapper with the root signal.
|
|
42
|
+
return wrapped(input.signal);
|
|
33
43
|
}
|
|
34
44
|
//# sourceMappingURL=executor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,yBAAyB,CAAC,CAAA;AA6BpE;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,KAAuB;IACtD,wEAAwE;IACxE,uEAAuE;IACvE,qEAAqE;IACrE,4CAA4C;IAC5C,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CACnC,CAAC,CAAC,EAAE,CAAE,CAA4D,CAAC,mBAAmB,CAAC,CACxF,CAAA;IACD,IAAI,SAAS,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,wEAAwE;YACxE,uDAAuD;YACvD,kEAAkE,CACnE,CAAA;IACH,CAAC;IAED,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,mDAAmD;IACnD,OAAO,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;AAC9B,CAAC"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// ─── Primary API ──────────────────────────────────────────────────────────────
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TotalTimeoutError = exports.TimeoutError = exports.InMemoryStore = exports.act = void 0;
|
|
4
|
+
exports.TotalTimeoutError = exports.TimeoutError = exports.isAsyncStore = exports.isSyncStore = exports.InMemoryStore = exports.REQUIRES_SYNC_STORE = exports.execute = exports.withStore = exports.invalidate = exports.act = void 0;
|
|
4
5
|
var act_js_1 = require("./core/act.js");
|
|
5
6
|
Object.defineProperty(exports, "act", { enumerable: true, get: function () { return act_js_1.act; } });
|
|
6
|
-
|
|
7
|
+
Object.defineProperty(exports, "invalidate", { enumerable: true, get: function () { return act_js_1.invalidate; } });
|
|
8
|
+
Object.defineProperty(exports, "withStore", { enumerable: true, get: function () { return act_js_1.withStore; } });
|
|
9
|
+
// ─── Execution engine (for custom policy chains) ──────────────────────────────
|
|
10
|
+
var executor_js_1 = require("./core/executor.js");
|
|
11
|
+
Object.defineProperty(exports, "execute", { enumerable: true, get: function () { return executor_js_1.execute; } });
|
|
12
|
+
Object.defineProperty(exports, "REQUIRES_SYNC_STORE", { enumerable: true, get: function () { return executor_js_1.REQUIRES_SYNC_STORE; } });
|
|
13
|
+
// ─── Stores ───────────────────────────────────────────────────────────────────
|
|
7
14
|
var memory_js_1 = require("./stores/memory.js");
|
|
8
15
|
Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return memory_js_1.InMemoryStore; } });
|
|
9
|
-
|
|
16
|
+
var base_js_1 = require("./stores/base.js");
|
|
17
|
+
Object.defineProperty(exports, "isSyncStore", { enumerable: true, get: function () { return base_js_1.isSyncStore; } });
|
|
18
|
+
Object.defineProperty(exports, "isAsyncStore", { enumerable: true, get: function () { return base_js_1.isAsyncStore; } });
|
|
19
|
+
// ─── Error classes ────────────────────────────────────────────────────────────
|
|
10
20
|
var timeout_js_1 = require("./policies/timeout.js");
|
|
11
21
|
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return timeout_js_1.TimeoutError; } });
|
|
12
22
|
Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return timeout_js_1.TotalTimeoutError; } });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export { act } from './core/act.js';
|
|
2
|
-
export type {
|
|
1
|
+
export { act, invalidate, withStore, } from './core/act.js';
|
|
2
|
+
export type { ScopedActSync, ScopedActAsync, } from './core/act.js';
|
|
3
|
+
export { execute, REQUIRES_SYNC_STORE, } from './core/executor.js';
|
|
3
4
|
export { InMemoryStore } from './stores/memory.js';
|
|
4
5
|
export type { InMemoryStoreOptions } from './stores/memory.js';
|
|
5
|
-
export {
|
|
6
|
+
export { isSyncStore, isAsyncStore, } from './stores/base.js';
|
|
7
|
+
export { TimeoutError, TotalTimeoutError, } from './policies/timeout.js';
|
|
8
|
+
export type { ActFn, ActResult, ActSuccess, ActFailure, ActSource, ActOptions, RetryOptions, TimeoutOptions, DedupeOptions, CacheOptions, PolicyApplier, PolicyContext, RunMeta, StateStore, // v1.0 alias for SyncStateStore (zero breakage)
|
|
9
|
+
SyncStateStore, AsyncStateStore, AnyStateStore, } from './types/index.js';
|
|
6
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,GAAG,EACH,UAAU,EACV,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,YAAY,EACV,aAAa,EACb,cAAc,GACf,MAAM,eAAe,CAAA;AAItB,OAAO,EACL,OAAO,EACP,mBAAmB,GACpB,MAAM,oBAAoB,CAAA;AAI3B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE9D,OAAO,EACL,WAAW,EACX,YAAY,GACb,MAAM,kBAAkB,CAAA;AAIzB,OAAO,EACL,YAAY,EACZ,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAI9B,YAAY,EAEV,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EAGV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EAGZ,aAAa,EACb,aAAa,EACb,OAAO,EAGP,UAAU,EAAS,gDAAgD;AACnE,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,kBAAkB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// ─── Primary API ──────────────────────────────────────────────────────────────
|
|
2
|
+
export { act, invalidate, withStore, } from './core/act.js';
|
|
3
|
+
// ─── Execution engine (for custom policy chains) ──────────────────────────────
|
|
4
|
+
export { execute, REQUIRES_SYNC_STORE, } from './core/executor.js';
|
|
5
|
+
// ─── Stores ───────────────────────────────────────────────────────────────────
|
|
3
6
|
export { InMemoryStore } from './stores/memory.js';
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
export { isSyncStore, isAsyncStore, } from './stores/base.js';
|
|
8
|
+
// ─── Error classes ────────────────────────────────────────────────────────────
|
|
9
|
+
export { TimeoutError, TotalTimeoutError, } from './policies/timeout.js';
|
|
6
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,OAAO,EACL,GAAG,EACH,UAAU,EACV,SAAS,GACV,MAAM,eAAe,CAAA;AAOtB,iFAAiF;AAEjF,OAAO,EACL,OAAO,EACP,mBAAmB,GACpB,MAAM,oBAAoB,CAAA;AAE3B,iFAAiF;AAEjF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,EACL,WAAW,EACX,YAAY,GACb,MAAM,kBAAkB,CAAA;AAEzB,iFAAiF;AAEjF,OAAO,EACL,YAAY,EACZ,iBAAiB,GAClB,MAAM,uBAAuB,CAAA"}
|
package/dist/policies/cache.cjs
CHANGED
|
@@ -3,41 +3,105 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.cachePolicy = cachePolicy;
|
|
4
4
|
const base_js_1 = require("../stores/base.js");
|
|
5
5
|
const NS = 'cache:';
|
|
6
|
+
const INFLIGHT_NS = '__inflight:cache:';
|
|
6
7
|
/**
|
|
7
|
-
* Short-
|
|
8
|
-
* On a miss,
|
|
8
|
+
* Short-circuit the entire downstream chain on a cache hit.
|
|
9
|
+
* On a miss, run `fn` and store the result with TTL.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
+
* # Single-flight (fixes cache stampede)
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* On a sync store, the policy also stores the in-flight Promise under a
|
|
14
|
+
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
15
|
+
* cache but find an in-flight Promise join it instead of launching duplicate
|
|
16
|
+
* work. This is the same mechanism `dedupePolicy` uses, applied internally
|
|
17
|
+
* to cache misses so users don't need to combine `cache` + `dedupe` to
|
|
18
|
+
* avoid stampedes.
|
|
19
|
+
*
|
|
20
|
+
* On an async store, single-flight is not possible (the same sync-store
|
|
21
|
+
* constraint as dedupe applies). Stampedes are a known limitation — document
|
|
22
|
+
* and pair with dedupe at a higher layer if you need single-flight semantics.
|
|
23
|
+
*
|
|
24
|
+
* # Fail-open writes
|
|
25
|
+
*
|
|
26
|
+
* If `store.set()` throws (e.g. Redis transient error), we swallow the error
|
|
27
|
+
* and return the value anyway. The caller gets their result; the next call
|
|
28
|
+
* will simply re-run `fn` and try to cache again. Caching is an optimisation,
|
|
29
|
+
* not a correctness requirement.
|
|
30
|
+
*
|
|
31
|
+
* # Cache hit semantics
|
|
32
|
+
*
|
|
33
|
+
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
34
|
+
* set to `0` — no work was performed. (v1.1.0 reported `attempts: 1` on
|
|
35
|
+
* cache hits, which was inconsistent with the documented meaning of
|
|
36
|
+
* `attempts`. v1.1.5 fixes this.)
|
|
37
|
+
*
|
|
38
|
+
* Failures are NEVER cached. Only successful values are stored.
|
|
16
39
|
*/
|
|
17
40
|
function cachePolicy(opts) {
|
|
18
|
-
return (fn, ctx) => async () => {
|
|
41
|
+
return (fn, ctx) => async (signal) => {
|
|
19
42
|
const key = NS + ctx.key;
|
|
43
|
+
const inflightKey = INFLIGHT_NS + ctx.key;
|
|
44
|
+
// ─── Sync store: fast path with single-flight ────────────────────────
|
|
20
45
|
if ((0, base_js_1.isSyncStore)(ctx.store)) {
|
|
21
|
-
//
|
|
46
|
+
// 1. Cache hit?
|
|
22
47
|
const hit = ctx.store.get(key);
|
|
23
48
|
if (hit) {
|
|
24
49
|
ctx.meta.source = 'cache';
|
|
50
|
+
ctx.meta.attempts = 0;
|
|
25
51
|
return hit.value;
|
|
26
52
|
}
|
|
27
|
-
|
|
28
|
-
ctx.store.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
// 2. In-flight single-flight hit? Join it.
|
|
54
|
+
const inflight = ctx.store.get(inflightKey);
|
|
55
|
+
if (inflight)
|
|
56
|
+
return inflight;
|
|
57
|
+
// 3. Originator: launch fn, cache on success (fail-open).
|
|
58
|
+
const promise = Promise.resolve(fn(signal)).then((value) => {
|
|
59
|
+
try {
|
|
60
|
+
ctx.store.set(key, { value }, opts.ttl);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Fail-open: cache write failure should not surface to caller.
|
|
64
|
+
// The value is still valid; next call will just re-run fn.
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}, (err) => { throw err; });
|
|
68
|
+
// Try to publish the in-flight promise so concurrent callers can
|
|
69
|
+
// join (single-flight). If store.set throws, single-flight is
|
|
70
|
+
// disabled for this call — concurrent callers will all run fn.
|
|
71
|
+
// That's still correct, just less efficient.
|
|
72
|
+
try {
|
|
73
|
+
ctx.store.set(inflightKey, promise);
|
|
74
|
+
// Cleanup the in-flight slot on settle. The `.catch(() => {})`
|
|
75
|
+
// suppresses the unhandled-rejection warning that would otherwise
|
|
76
|
+
// fire on the `.finally()` chain — the original `promise` (with
|
|
77
|
+
// its rejection) is returned to the caller, which has its own
|
|
78
|
+
// try/catch via act().
|
|
79
|
+
promise.finally(() => {
|
|
80
|
+
try {
|
|
81
|
+
ctx.store.delete(inflightKey);
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
}).catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Single-flight unavailable; proceed without publishing.
|
|
37
88
|
}
|
|
38
|
-
|
|
89
|
+
return promise;
|
|
90
|
+
}
|
|
91
|
+
// ─── Async store: no single-flight (race window unavoidable) ─────────
|
|
92
|
+
const hit = await ctx.store.get(key);
|
|
93
|
+
if (hit) {
|
|
94
|
+
ctx.meta.source = 'cache';
|
|
95
|
+
ctx.meta.attempts = 0;
|
|
96
|
+
return hit.value;
|
|
97
|
+
}
|
|
98
|
+
const value = await fn(signal);
|
|
99
|
+
try {
|
|
39
100
|
await ctx.store.set(key, { value }, opts.ttl);
|
|
40
|
-
return value;
|
|
41
101
|
}
|
|
102
|
+
catch {
|
|
103
|
+
// Fail-open: see sync path comment.
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
42
106
|
};
|
|
43
107
|
}
|
package/dist/policies/cache.d.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import type { CacheOptions, PolicyApplier } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Short-
|
|
4
|
-
* On a miss,
|
|
3
|
+
* Short-circuit the entire downstream chain on a cache hit.
|
|
4
|
+
* On a miss, run `fn` and store the result with TTL.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* # Single-flight (fixes cache stampede)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* On a sync store, the policy also stores the in-flight Promise under a
|
|
9
|
+
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
10
|
+
* cache but find an in-flight Promise join it instead of launching duplicate
|
|
11
|
+
* work. This is the same mechanism `dedupePolicy` uses, applied internally
|
|
12
|
+
* to cache misses so users don't need to combine `cache` + `dedupe` to
|
|
13
|
+
* avoid stampedes.
|
|
14
|
+
*
|
|
15
|
+
* On an async store, single-flight is not possible (the same sync-store
|
|
16
|
+
* constraint as dedupe applies). Stampedes are a known limitation — document
|
|
17
|
+
* and pair with dedupe at a higher layer if you need single-flight semantics.
|
|
18
|
+
*
|
|
19
|
+
* # Fail-open writes
|
|
20
|
+
*
|
|
21
|
+
* If `store.set()` throws (e.g. Redis transient error), we swallow the error
|
|
22
|
+
* and return the value anyway. The caller gets their result; the next call
|
|
23
|
+
* will simply re-run `fn` and try to cache again. Caching is an optimisation,
|
|
24
|
+
* not a correctness requirement.
|
|
25
|
+
*
|
|
26
|
+
* # Cache hit semantics
|
|
27
|
+
*
|
|
28
|
+
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
29
|
+
* set to `0` — no work was performed. (v1.1.0 reported `attempts: 1` on
|
|
30
|
+
* cache hits, which was inconsistent with the documented meaning of
|
|
31
|
+
* `attempts`. v1.1.5 fixes this.)
|
|
32
|
+
*
|
|
33
|
+
* Failures are NEVER cached. Only successful values are stored.
|
|
12
34
|
*/
|
|
13
35
|
export declare function cachePolicy<T>(opts: CacheOptions): PolicyApplier<T>;
|
|
14
36
|
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAY1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAuEnE"}
|
package/dist/policies/cache.js
CHANGED
|
@@ -1,41 +1,105 @@
|
|
|
1
1
|
import { isSyncStore } from '../stores/base.js';
|
|
2
2
|
const NS = 'cache:';
|
|
3
|
+
const INFLIGHT_NS = '__inflight:cache:';
|
|
3
4
|
/**
|
|
4
|
-
* Short-
|
|
5
|
-
* On a miss,
|
|
5
|
+
* Short-circuit the entire downstream chain on a cache hit.
|
|
6
|
+
* On a miss, run `fn` and store the result with TTL.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* # Single-flight (fixes cache stampede)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* On a sync store, the policy also stores the in-flight Promise under a
|
|
11
|
+
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
12
|
+
* cache but find an in-flight Promise join it instead of launching duplicate
|
|
13
|
+
* work. This is the same mechanism `dedupePolicy` uses, applied internally
|
|
14
|
+
* to cache misses so users don't need to combine `cache` + `dedupe` to
|
|
15
|
+
* avoid stampedes.
|
|
16
|
+
*
|
|
17
|
+
* On an async store, single-flight is not possible (the same sync-store
|
|
18
|
+
* constraint as dedupe applies). Stampedes are a known limitation — document
|
|
19
|
+
* and pair with dedupe at a higher layer if you need single-flight semantics.
|
|
20
|
+
*
|
|
21
|
+
* # Fail-open writes
|
|
22
|
+
*
|
|
23
|
+
* If `store.set()` throws (e.g. Redis transient error), we swallow the error
|
|
24
|
+
* and return the value anyway. The caller gets their result; the next call
|
|
25
|
+
* will simply re-run `fn` and try to cache again. Caching is an optimisation,
|
|
26
|
+
* not a correctness requirement.
|
|
27
|
+
*
|
|
28
|
+
* # Cache hit semantics
|
|
29
|
+
*
|
|
30
|
+
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
31
|
+
* set to `0` — no work was performed. (v1.1.0 reported `attempts: 1` on
|
|
32
|
+
* cache hits, which was inconsistent with the documented meaning of
|
|
33
|
+
* `attempts`. v1.1.5 fixes this.)
|
|
34
|
+
*
|
|
35
|
+
* Failures are NEVER cached. Only successful values are stored.
|
|
13
36
|
*/
|
|
14
37
|
export function cachePolicy(opts) {
|
|
15
|
-
return (fn, ctx) => async () => {
|
|
38
|
+
return (fn, ctx) => async (signal) => {
|
|
16
39
|
const key = NS + ctx.key;
|
|
40
|
+
const inflightKey = INFLIGHT_NS + ctx.key;
|
|
41
|
+
// ─── Sync store: fast path with single-flight ────────────────────────
|
|
17
42
|
if (isSyncStore(ctx.store)) {
|
|
18
|
-
//
|
|
43
|
+
// 1. Cache hit?
|
|
19
44
|
const hit = ctx.store.get(key);
|
|
20
45
|
if (hit) {
|
|
21
46
|
ctx.meta.source = 'cache';
|
|
47
|
+
ctx.meta.attempts = 0;
|
|
22
48
|
return hit.value;
|
|
23
49
|
}
|
|
24
|
-
|
|
25
|
-
ctx.store.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
// 2. In-flight single-flight hit? Join it.
|
|
51
|
+
const inflight = ctx.store.get(inflightKey);
|
|
52
|
+
if (inflight)
|
|
53
|
+
return inflight;
|
|
54
|
+
// 3. Originator: launch fn, cache on success (fail-open).
|
|
55
|
+
const promise = Promise.resolve(fn(signal)).then((value) => {
|
|
56
|
+
try {
|
|
57
|
+
ctx.store.set(key, { value }, opts.ttl);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fail-open: cache write failure should not surface to caller.
|
|
61
|
+
// The value is still valid; next call will just re-run fn.
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}, (err) => { throw err; });
|
|
65
|
+
// Try to publish the in-flight promise so concurrent callers can
|
|
66
|
+
// join (single-flight). If store.set throws, single-flight is
|
|
67
|
+
// disabled for this call — concurrent callers will all run fn.
|
|
68
|
+
// That's still correct, just less efficient.
|
|
69
|
+
try {
|
|
70
|
+
ctx.store.set(inflightKey, promise);
|
|
71
|
+
// Cleanup the in-flight slot on settle. The `.catch(() => {})`
|
|
72
|
+
// suppresses the unhandled-rejection warning that would otherwise
|
|
73
|
+
// fire on the `.finally()` chain — the original `promise` (with
|
|
74
|
+
// its rejection) is returned to the caller, which has its own
|
|
75
|
+
// try/catch via act().
|
|
76
|
+
promise.finally(() => {
|
|
77
|
+
try {
|
|
78
|
+
ctx.store.delete(inflightKey);
|
|
79
|
+
}
|
|
80
|
+
catch { /* ignore */ }
|
|
81
|
+
}).catch(() => { });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Single-flight unavailable; proceed without publishing.
|
|
34
85
|
}
|
|
35
|
-
|
|
86
|
+
return promise;
|
|
87
|
+
}
|
|
88
|
+
// ─── Async store: no single-flight (race window unavoidable) ─────────
|
|
89
|
+
const hit = await ctx.store.get(key);
|
|
90
|
+
if (hit) {
|
|
91
|
+
ctx.meta.source = 'cache';
|
|
92
|
+
ctx.meta.attempts = 0;
|
|
93
|
+
return hit.value;
|
|
94
|
+
}
|
|
95
|
+
const value = await fn(signal);
|
|
96
|
+
try {
|
|
36
97
|
await ctx.store.set(key, { value }, opts.ttl);
|
|
37
|
-
return value;
|
|
38
98
|
}
|
|
99
|
+
catch {
|
|
100
|
+
// Fail-open: see sync path comment.
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
39
103
|
};
|
|
40
104
|
}
|
|
41
105
|
//# sourceMappingURL=cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,MAAM,EAAE,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"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"}
|